Skip to content

feat: taint the keys of a **mapping as the keyword names of the call - #196

Merged
SizzleUnrlsd merged 3 commits into
mainfrom
feat/keyword-name-conditions
Sep 30, 2026
Merged

SizzleUnrlsd merged 3 commits into
mainfrom
feat/keyword-name-conditions

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #193.

Django's ORM takes column aliases and lookups as keyword names; five advisories are SQL injections through those names when a project expands an attacker-controlled mapping. The engine reported an argument passed through ** with neither position nor keyword, right for the mapping's values, and had no notion that its keys become keyword names, so a curated entry point could not say "reached when the attacker chooses a keyword name", and a project expanding {request.GET["sort"]: F("id")} looked like one expanding a constant mapping.

  • Key taint: a third heap field, keys, beside elements and attributes, filled by the taint engine only (the points-to solver never does) for a dict literal, dict(...) (a copied mapping, dict(**m), dict([(k, v)])), a comprehension (already lowered to a subscript assignment) and d[k] = v; {**d} copies it. It is not part of the deep taint, so no existing flow changes.
  • A way of passing: f(**m) with tainted keys adds (None, "**") (KEYWORD_NAMES) to the flow's passed_as, for advisory sinks only: no regular sink reads a keyword name.
  • Call sites: Arguments.keyword_unpacked says a call expands a mapping whose keys it does not write; a ** literal with constant string keys that nothing else uses is folded into keywords, so given(), argument conditions and safe arguments decide it (more precise than before). Cache format 12.
  • The advisory condition keyword_name (text only): met on the (None, "**") way, pending when the call expands a mapping whose keys are not established (a parameter, a mapping built elsewhere, dict(zip(...)), a literal used elsewhere), contradicted by a call writing every keyword name, which ruled_out reports as annotate(keywords=total). With the issue's reproduction and Django 5.2.5 pinned, listing is exploitable with the condition met and fixed alone leaves the advisory not_affected.

One choice beyond the design: _keyword_entries only folds a literal the call alone uses, the guard summaries._literal_entries already has; without it m = {}; m[alias] = F("id"); annotate(**m) would be ruled out at reachability while the taint stage finds it exploitable. A literal used elsewhere is therefore pending rather than contradicted.

Documented limits: keys through a project wrapper stay pending; update, setdefault, keys() and iteration carry no key taint; a Phi-bound mapping is unknown. Curated entry points naming QuerySet.annotate do not yet match Model.objects.annotate (derived as a project symbol): a separate issue.

Tests: tests/test_keyword_name_conditions.py (20 cases) and tests/test_key_taint.py (26), failing before the change; pytest -m "not regression" 1581 passed, 2 skipped; regression corpus 19 passed under Python 3.12, snapshots unchanged; ruff check . and mypy clean.

Add the keys heap field and key taint for dict literals, dict(...), comprehensions and subscript assignment; report the keys of a ** argument as the (None, "**") way of an advisory flow; record Arguments.keyword_unpacked and fold a ** literal with constant keys that nothing else uses into the keywords (cache format 12); decide the new keyword_name advisory condition from them. Closes #193.
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Sep 30, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 30, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit d3d5f7e into main Sep 30, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Taint the keys of a **mapping expansion as the keyword names of the call

1 participant