From 8a09d6e4b2a07f9d86fb1296a465a70247769b17 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 22 Sep 2026 00:41:57 +0200 Subject: [PATCH 1/2] test: define the NOSQL, LOG and PII taint kinds --- tests/test_taint.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_taint.py b/tests/test_taint.py index d2bf0a3..b0e5bbc 100644 --- a/tests/test_taint.py +++ b/tests/test_taint.py @@ -101,6 +101,38 @@ def test_taint_kinds_form_a_bitset() -> None: assert {TaintKind.SQL, TaintKind.COMMAND, TaintKind.HTML, TaintKind.PATH, TaintKind.SSRF} <= set(TaintKind) +def test_nosql_is_ordinary_input_while_log_and_pii_are_opt_in() -> None: + assert TaintKind.NOSQL in TaintKind.ALL + assert TaintKind.LOG not in TaintKind.ALL + assert TaintKind.PII not in TaintKind.ALL + assert TaintKind.CREDENTIAL not in TaintKind.ALL + + +def test_ordinary_input_reaches_a_nosql_sink_but_not_a_log_sink() -> None: + registry = SecurityModelRegistry() + registry.register( + Source(sym("python.flask.request.args"), HTTP), + Source(sym("python.app.record"), "record", TaintKind.LOG), + Sink(sym("python.db.find"), TaintKind.NOSQL), + Sink(sym("python.logging.info"), TaintKind.LOG), + ) + _, facts = analyze( + "from flask import request\n" + "import db, logging\n" + "from app import record\n\n" + "def run():\n" + " db.find(request.args['q'])\n" + " logging.info(request.args['q'])\n" + " logging.info(record())\n", + registry.freeze(), + ) + + assert [(f.sink.symbol, f.kinds) for f in facts.flows] == [ + (sym("python.db.find"), TaintKind.NOSQL), + (sym("python.logging.info"), TaintKind.LOG), + ] + + def test_taint_joins_with_or() -> None: a = Taint(TaintKind.SQL, frozenset({Source(sym("python.a"), "a")})) b = Taint(TaintKind.HTML, frozenset({Source(sym("python.b"), "b")})) From c1333a42d2bd96e1718d4370ed9404c50e7cc965 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 22 Sep 2026 00:41:57 +0200 Subject: [PATCH 2/2] feat: add the NOSQL, LOG and PII taint kinds TaintKind is a Flag a plugin cannot extend, so NoSQL injection, log injection and personal-data flows had to reuse another rule's kind. NOSQL joins ALL; LOG and PII stay outside it like CREDENTIAL, so ordinary input reaching a logger is not a finding until a model marks the value. Closes #96 --- CHANGELOG.md | 6 ++++++ docs/plugins.md | 12 ++++++++---- src/coretrace_python/taint/models.py | 10 +++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 312ca68..1810d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Analysis + +- New taint kinds: `NOSQL` in `TaintKind.ALL`; `LOG` and `PII` outside it, carried only by the values a model marks, so a NoSQL-injection, log-injection or personal-data detector no longer has to share a kind with another rule (#96). + ## 0.2.0 (2026-09-06) ### Adoption in an existing repository diff --git a/docs/plugins.md b/docs/plugins.md index 2013e3b..770e9fb 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -101,10 +101,14 @@ class CommandInjectionPlugin(TaintDetector): ``` `confidence` defaults to high; a hotspot, a flow behind a guard that mentions the value -or an authorization decorator, is reported at medium. The taint kinds are `SQL`, -`COMMAND`, `HTML`, `PATH`, `SSRF`, `CODE`, `DESERIALIZATION`, `REDIRECT`, `ADVISORY` -(APIs affected by advisories) and `CREDENTIAL`, which only credential-named parameters -carry so that ordinary input reaching a database write is not a plaintext credential. +or an authorization decorator, is reported at medium. The taint kinds every source +carries by default (`TaintKind.ALL`) are `SQL`, `NOSQL`, `COMMAND`, `HTML`, `PATH`, +`SSRF`, `CODE`, `DESERIALIZATION`, `REDIRECT` and `ADVISORY` (APIs affected by +advisories). Three kinds are outside `ALL`, so only a value a model marks with them +reaches their sinks: `CREDENTIAL`, which credential-named parameters carry so that +ordinary input reaching a database write is not a plaintext credential; `LOG`, for +values that must not be written to a log as they are; and `PII`, for personal data +that must not leave the application through a log, a third party or plain storage. ### Security models: `ModelPlugin` diff --git a/src/coretrace_python/taint/models.py b/src/coretrace_python/taint/models.py index 1c212bd..6b420db 100644 --- a/src/coretrace_python/taint/models.py +++ b/src/coretrace_python/taint/models.py @@ -30,10 +30,14 @@ class TaintKind(Flag): ADVISORY = auto() DESERIALIZATION = auto() REDIRECT = auto() - ALL = SQL | COMMAND | HTML | PATH | SSRF | CODE | ADVISORY | DESERIALIZATION | REDIRECT - # Outside ALL on purpose: only credential-named parameters carry it, so a database - # write reached by ordinary input is not a plaintext credential. + NOSQL = auto() + ALL = SQL | COMMAND | HTML | PATH | SSRF | CODE | ADVISORY | DESERIALIZATION | REDIRECT | NOSQL + # Outside ALL on purpose: only the values a model marks carry them, so a database + # write reached by ordinary input is not a plaintext credential, and ordinary input + # reaching a logger is neither log forging nor a personal-data leak. CREDENTIAL = auto() + LOG = auto() + PII = auto() class ModelError(Exception):