Skip to content

feat: let advisory entry points name the arguments the attacker must control - #147

Merged
SizzleUnrlsd merged 2 commits into
mainfrom
feat/advisory-attacker-arguments
Sep 25, 2026
Merged

SizzleUnrlsd merged 2 commits into
mainfrom
feat/advisory-attacker-arguments

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #145.

An advisory entry point was an ADVISORY sink on every argument, so attacker data in any argument made the call exploitable-vulnerability (CRITICAL). An entry point may now name the arguments the attacker must control.

"attacker_arguments": [{"argument": "url", "position": 0}]
  • argument is the keyword name; position counts as for argument conditions (from 0, a method's receiver excluded).
  • "variadic": true extends a position to every later one, as for safe_join(directory, *pathnames): {"position": 1, "variadic": true}.
  • Attacker input in another argument leaves the call reachable. Reachability and VEX affected do not change.
  • An entry point that names no argument keeps today's behaviour. Existing data stays valid, and the new field round-trips through advisory files.

*args and **kwargs

An argument unpacked with * or ** has no position and no keyword, so the engine cannot tell which argument it fills. It counts, for two reasons:

  • The engine already treats unknown arguments this way. An unpacked argument leaves an argument condition pending review and does not rule the call out.
  • The other choice would lose what 0.8.0 finds today. A wrapper such as def fetch(*args, **kwargs): return requests.get(*args, **kwargs) would no longer reach the URL.

The cost: fetch(URL, **{'data': attacker}) stays exploitable. Detector sinks make the opposite choice (Sink.kinds_at gives an unpacked argument no restricted kind), because a sink model states what it accepts; here the question is whether the attacker controls the argument at all. Tests pin both the rule and the wrapper case, and a mutation to "does not count" makes 4 of them fail.

Design

  • report() already knew the position or keyword of the tainted argument, for direct calls and through function summaries. TaintFlow now keeps it in passed_as.
  • Flows that differ only in passed_as are merged. Without this, a value passed twice (f(x, key=x)) would become two flows, and detectors would report it twice. They see exactly the flows they saw before.
  • correlate() checks each flow against the entry point of each advisory, so two advisories on the same symbol can name different arguments.
  • No cache format change: flows are not cached, and the configuration key already holds repr(advisories).

Measured

The curated CVE-2024-49766 data was taken from the plugins repository, and attacker_arguments was added in a scratch copy, not in this PR:

  • send_from_directory: path, position 1;
  • safe_join: position 1, variadic.
Call (Flask app) 0.8.0 this branch
send_from_directory("uploads", name), route <path:name> exploitable exploitable
send_from_directory("reports", "report.pdf", download_name=request.args["name"]) exploitable reachable
send_from_directory(request.args["dir"], "report.pdf") exploitable reachable

Out of scope

Keys versus values of a ** mapping. Django's _connector (CVE-2025-64459) needs the keys, and taint does not tell them apart.

Tests

  • tests/test_attacker_arguments.py, 21 tests:
    • by keyword, by position, and on an instance method;
    • variadic parameters;
    • another argument, which leaves the call reachable;
    • unpacked arguments and the wrapper case;
    • a flow through a project function, where the argument of the entry-point call decides;
    • two advisories on one symbol;
    • the file round-trip, and six malformed declarations.
  • Full suite: 1101 passed, 2 skipped. Regression snapshots under 3.12: 19 passed, unchanged. mypy and ruff: clean.
  • The enterprise suite passes against this branch (173 passed).

…control

An advisory entry point was an ADVISORY sink on every argument, so
attacker data in any argument made the call exploitable. An entry point
may now name its attacker_arguments, by keyword and position, a variadic
one taking every later position; attacker input elsewhere leaves the
call reachable. Each taint flow records how its value is passed to the
sink, and flows differing only in that are merged, so detectors see the
same flows as before.
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Sep 25, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 25, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit bdeb00d into main Sep 25, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/advisory-attacker-arguments branch September 25, 2026 17:01
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.

Advisories: an entry point names the argument the attacker must control

1 participant