feat(rules): Django request sources; a destination with a fixed origin is not untrusted

python.toml's sources were Flask-only, so no flow could start anywhere on a Django
project. OpenCVE's SSRF reads its URL from `form.cleaned_data`, and the scan answered
the sink file and found nothing. Added: request.GET/POST/body/COOKIES/FILES/META,
DRF's query_params, and cleaned_data.

With those sources known, OpenCVE reported three redirects that cannot leave the
site: `reverse("cves") + "?" + request.GET.urlencode()` (twice) and
`reverse(route, kwargs=...)`. Destination sink facts can now set
`prefix_fixes_origin` and `origin_anchors`. A path is cut when it enters a
concatenation, f-string or `+=` through anything other than its leftmost operand,
and that operand fixes the origin. Origin-fixing operands:
- a literal `/x`, `?` or `#`;
- a literal of the form scheme://host/;
- an anchor call;
- a variable whose every assignment fixes the origin.
A path is also cut when it passes through an anchor. `"/" + x` (//evil) and
`"https://" + host` stay reported. A Joern-gated test covers all seven shapes.

Exploratory (retune after protocol-v1): OpenCVE vulnerable and fixed pins are both
0 findings, down from 2 and 3. The SSRF itself is still missed, because the flow
crosses a registry dispatch (`REGISTRY.get(t)(extras).run()`) that the engine does
not follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-25 12:37:22 +02:00
co-authored by Claude Opus 5.5
parent ec3d1457fb
commit 562b670a43
7 changed files with 209 additions and 3 deletions
+11
View File
@@ -118,6 +118,10 @@ class TaintSpec:
# Which argument of each apply call its RESULT comes from, as `call:position`; position 0 means the call
# returns nothing a flow can carry. Only calls a fact describes appear here.
dispatch_value: tuple[str, ...] = ()
# Whether a prefix that fixes the origin makes a destination safe, and the calls that build such a
# prefix (`reverse`). From the sink facts; only destination families set them.
fixed_origin: bool = False
origin_anchors: tuple[str, ...] = ()
@dataclass(frozen=True)
@@ -255,6 +259,8 @@ def taint_specs(
safe_shapes = tuple(sorted({call for fact in scoped.sanitizers if _is_shape(fact) for call in fact.calls}))
by_family: dict[str, set[str]] = {}
bounded: dict[str, set[str]] = {}
fixed_origin: set[str] = set()
anchors: dict[str, set[str]] = {}
for sink in scoped.sinks:
family = families.family_of_cwe(sink.cwe)
if family is None:
@@ -264,6 +270,9 @@ def taint_specs(
# is on the fact already, so which sinks these are is read from the data rather than declared here.
if sink.cwe in OVERFLOW_CWES:
bounded.setdefault(family.id, set()).update(sink.calls)
if sink.prefix_fixes_origin:
fixed_origin.add(family.id)
anchors.setdefault(family.id, set()).update(sink.origin_anchors)
return {
family_id: TaintSpec(
family=family_id,
@@ -275,6 +284,8 @@ def taint_specs(
safe_shape_sinks=safe_shapes,
dispatch_apply=dispatch_apply,
dispatch_value=dispatch_value,
fixed_origin=family_id in fixed_origin,
origin_anchors=tuple(sorted(anchors.get(family_id, ()))),
)
for family_id, calls in sorted(by_family.items())
}
+2
View File
@@ -60,6 +60,8 @@ def request_params(
# `apply_filters` is, which makes a general mechanism into a WordPress feature.
"dispatchApply": spec.dispatch_apply,
"dispatchValue": spec.dispatch_value,
"fixedOrigin": "true" if spec.fixed_origin else "false",
"originAnchors": spec.origin_anchors,
}