mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 04:59:28 +00:00
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:
co-authored by
Claude Opus 5.5
parent
ec3d1457fb
commit
562b670a43
@@ -42,6 +42,12 @@ class SinkFact:
|
||||
language: str
|
||||
weak_literals: tuple[str, ...] = ()
|
||||
format_arg: int | None = None
|
||||
# A DESTINATION sink (a redirect, an outbound request) is dangerous only where the value can choose where
|
||||
# it goes. With `prefix_fixes_origin`, a value appended after a prefix that already fixes the origin -- a
|
||||
# path `/x`, a `?query`, `scheme://host/`, or an `origin_anchors` call such as Django's `reverse()` -- is
|
||||
# not an untrusted destination, and neither is one that only fills an anchor call's arguments.
|
||||
prefix_fixes_origin: bool = False
|
||||
origin_anchors: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -263,6 +269,14 @@ def _sources(value: object, language: str, path: Path) -> list[SourceFact]:
|
||||
return rows
|
||||
|
||||
|
||||
def _flag(value: object, field: str, path: Path, ident: str) -> bool:
|
||||
if value is None:
|
||||
return False
|
||||
if not isinstance(value, bool):
|
||||
raise FactLoadError(f"{path} sink {ident} has a non-boolean {field}")
|
||||
return value
|
||||
|
||||
|
||||
def _sinks(value: object, language: str, path: Path) -> list[SinkFact]:
|
||||
rows: list[SinkFact] = []
|
||||
for item in _items(value):
|
||||
@@ -287,6 +301,8 @@ def _sinks(value: object, language: str, path: Path) -> list[SinkFact]:
|
||||
language=language,
|
||||
weak_literals=_strings(item.get("weak_literals"), "weak_literals", path),
|
||||
format_arg=format_arg if isinstance(format_arg, int) else None,
|
||||
prefix_fixes_origin=_flag(item.get("prefix_fixes_origin"), "prefix_fixes_origin", path, ident),
|
||||
origin_anchors=_strings(item.get("origin_anchors"), "origin_anchors", path),
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
Reference in New Issue
Block a user