Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Two plugins may declare the same symbol: an identical model is registered once and sinks merge their kinds and positions; a real conflict names both plugins instead of failing the run on the first duplicate (#92).

### 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
Expand Down
12 changes: 8 additions & 4 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
10 changes: 7 additions & 3 deletions src/coretrace_python/taint/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
32 changes: 32 additions & 0 deletions tests/test_taint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")}))
Expand Down
Loading