Skip to content

feat: judge SSRF flows on the destination their URL's constant text fixes - #166

Merged
SizzleUnrlsd merged 2 commits into
mainfrom
feat/ssrf-destinations
Sep 26, 2026
Merged

SizzleUnrlsd merged 2 commits into
mainfrom
feat/ssrf-destinations

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Summary

ssrf now judges each flow on how + and f-strings build its URL. A fixed host is treated as a limited proof, not as a proof that there is no SSRF. Everything the analysis does not prove stays reported.

Closes #165.

URL Verdict Why
Constant text fixes scheme://host and ends the authority with /, ? or # before any input: "https://api.example.com/users/" + uid, f"https://api.example.com/users/{uid}", API + "users/" + uid hotspot The input cannot choose the host. It still chooses the path, which may reach a sensitive resource of that host, and a redirect may lead elsewhere.
The constant text fixes the path too (the input reaches only the query or fragment), and the call disables redirects: allow_redirects=False, follow_redirects=False refuted The destination is fixed. A client honours the keyword it knows and rejects the other with a TypeError, so either way no redirect is followed.
Fixed destination with redirects followed, or options unpacked with ** hotspot A redirect may lead elsewhere.
Anything else vulnerability, as before Not proven: a base of unknown value (an import, a call result), which may leave the authority open ("https:/" + "/" + path); a constant leaving it open (no / after the host, a variable port, no scheme); urljoin; %; format; a URL built in a helper function.

A module-level name counts as known text only when the module binds it once, to a string literal, and never rebinds it:

  • no second assignment or for target in the module body;
  • no function assigning it as global;
  • no definition of that name.

Design

  • abstract/strings.py. leading_text(value, defs, strings) gives the constant text a value starts with, reading only + and f-strings. ModuleStringsAnalysis (abstract.module_strings) gives the module-level strings bound once. It counts every StoreLocal of the module body, every for target (ForNext) and every SetGlobal. A module that does not lower entirely has none.
  • TaintDetector.judge(ctx, function, flow, verdict). A new hook, which by default returns the refutation's verdict. ssrf overrides it, so only this rule's verdict changes. The advisory correlation reads the same flow, and it keeps the refutation's verdict; a test checks that the exploitable-vulnerability of a fixed-host call is still a vulnerability. Open redirects and the advisories that need the host are out of scope (Precision: an SSRF flow into a URL whose constant text fixes the host is a hotspot #165).
  • The ssrf manifest now requires ir.ssa and abstract.module_strings. test_shipped_plugins_load_with_their_manifests states it, instead of listing ssrf among the rules requiring flows and refutation only.

Corpus

The regression snapshots, which record each verdict, are unchanged. No ssrf finding is refuted or downgraded:

Finding URL Verdict
InsecureGram app/ssrf.py:12 requests.get(url), with url: str = Query(...) vulnerability: the whole URL is input
pygoat introduction/views.py:963 url = request.POST["url"] vulnerability: the whole URL is input
vulnerable-flask app.py:60 requests.get(request.args["url"]) vulnerability: the whole URL is input
httpie httpie/client.py:114 requests_session.send(prepared_request) vulnerability: no string built here
Vulnerable_fastapi exploit_scripts/stored_xss_profile.py:24 and :28 f"{BASE_URL}/profile/{user_id}", with BASE_URL imported from _common vulnerability: a base of unknown value

In the last case, _common.BASE_URL is "http://127.0.0.1:8000". Even read, it would only give a hotspot: the fixed host is internal, and the input chooses the path.

Verification

  • tests/test_ssrf_destinations.py, 25 tests: the six variants, unknown and incomplete bases, module strings bound more than once (a second literal, global, a for target, a definition), redirects (refuted, followed, disabled with the path chosen, unpacked options), the evidence text, a helper function, and the advisory verdict. The 11 tests expecting a hotspot or a refutation failed before the change.
  • pytest: 1220 passed. pytest -m regression: 19 passed, unchanged. ruff check and mypy: clean.
  • The Enterprise plugins pass on this branch (257 tests).

The ssrf rule now also reads how the URL of each flow is built, so its manifest requires the SSA form and the module's strings.
…ixes

A fixed host makes a flow a hotspot; a fixed host and path with redirects disabled refutes it. Only + and f-strings are read, over string literals and module-level strings bound once. The rule refines its own verdict through TaintDetector.judge, so the advisory correlation keeps the refutation's.
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Sep 26, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 26, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit 4b016e5 into main Sep 26, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/ssrf-destinations branch September 26, 2026 02:15
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.

Precision: an SSRF flow into a URL whose constant text fixes the host is a hotspot

1 participant