Skip to content

feat: reach advisory entry points marked read through attribute reads - #125

Merged
SizzleUnrlsd merged 2 commits into
mainfrom
feat/read-entry-points
Sep 24, 2026
Merged

SizzleUnrlsd merged 2 commits into
mainfrom
feat/read-entry-points

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #111.

Some advisory entry points are attributes whose getter runs the vulnerable code. Werkzeug parses a multipart body when request.form, request.files, request.data or request.values is read (CVE-2023-25577). Until now, reachable-vulnerability only looked at call sites. A project that reads request.form["name"] or runs for key in request.files was never reported, and curated data could only list the call forms it knew of (request.form.get(...)).

Change

  • Schema. An entry point can carry "read": true, which is validated as a boolean. AdvisoryEntryPoint.read defaults to false, so existing files mean what they meant. I named it read instead of property to describe how the entry point is reached, and to avoid shadowing the builtin.
  • Call graph. CallGraph.reads(function) returns, as SymbolRead records, the symbols a function reads, whether it calls them or not. A read is a Symbol or GetAttr instruction whose value denotes a symbol, recorded where the function first reads it. This covers annotated parameters (request: Request) and inherited attributes, because it reuses the call graph's own symbol map.
  • reachable-vulnerability. A read of an entry point marked read, or of an attribute of one, reaches it. request.form.get reads request.form, and the lowering folds that chain into a single python.flask.request.form.get symbol, so the plugin also checks every prefix of the read symbol. It reports one finding per function and entry point, where the function first reads it. A read entry point that is also called is reported once, through the read. An entry point without read is still reached by a call only.
  • Cache. Modules served from the cache keep their reads, and the cache format goes from 7 to 8.

Out of scope

  • exploitable. A read entry point has no argument for attacker data to flow into, so it stops at reachable. Whether the reading route is exposed is the job of exposure-prioritization.
  • Curated data. The Werkzeug entry points in the enterprise feed move to read in a separate PR on that repository.

Cost

On healthchecks, the largest corpus project, the cached reads take 5.0 MB of a 22.7 MB cache (20,633 reads), next to 6.2 MB for call sites. Every read is kept. Filtering, for example leaving out builtins, would be a heuristic that makes the reads API less general.

Verification

  • pytest (the default selection): 983 passed.
  • pytest -m regression under Python 3.12, the CI interpreter: 19 passed, with every snapshot unchanged. No bundled advisory marks an entry point read.
  • ruff check src tests and mypy are clean.
  • I mutation-checked the tests. Each of these changes fails at least one test: dropping the per-function deduplication, reporting read entry points from calls too, reading only the full symbol and not its prefixes, ignoring the read flag, and not rebuilding reads from the cache.

@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 24, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit 87c0d60 into main Sep 24, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/read-entry-points branch September 24, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Advisories: reachability through an attribute read, not only a call

1 participant