CVE-2024-23334:aiohttp: Path traversal
A historical CVE, retraced through the questions Lachesis asks of vulnerable code.
A request path escaped the directory aiohttp was supposed to serve, and the server returned the file it found there.
That is the shape of the failure. The rest of this case file follows the evidence behind it.
What was known.
What Lachesis established.
Known before Lachesis ran
The CVE and vulnerable release were selected from public history. We are not claiming novel discovery.
- CVE
- CVE-2024-23334
- Vulnerable target
- v3.9.1 (affected: >= 1.0.5, < 3.9.2) · vulnerable
- Prior knowledge
- CVE-2024-23334 · v3.9.1 (affected: >= 1.0.5, < 3.9.2) is vulnerable
Independent re-detection
The experiment graph was built around the vulnerable file aiohttp/web_urldispatcher.py (release v3.9.1, commit 6333c026), rather than starting from arbitrary aiohttp source. We did not point the tool at the CVE. Enumeration ran over the whole candidate registry (31 constructors, every family) with the pass-3 guard-differential ranker ordering leads; the filesystem path sink surfaced on its own alongside the navigation and lifecycle families, and Lachesis traced the request path into the static-file open. Every field under sast_output is the enumerator's own emission (candidate capsule plus sources_of value-flow cone); everything under adjudication is my reading of the source and fix and is labelled as such.
- Seeded inputs
- None
- Run timestamp
- 2026-08-24T12:06:09Z
- Evidence artifact
- ~/.lachesis/graphs/aiohttp_static.kuzu
The commands and outputs below come from this recorded Lachesis run.
What Lachesis reconstructed.
One historical repository. Four captured queries. A complete source-to-sink argument.
Lachesis follows the same evidence path through unfamiliar repositories.
Here is how the path becomes visible.
These are the recorded questions Lachesis asked of the historical vulnerable code, followed by the raw result and source location each query returned.
Captured runThis is a real replay of Lachesis over the graph we built for this case. Every command below was run against the aiohttp_static.kuzu graph and every result is the output captured on that drive. We did not point the tool at a file or a family. The hunt listed the whole taxonomy first and the path containment sink came out of that. No request or payload was seeded; the graph was built from the source alone and the finding was rediscovered from graph structure, so the mode is independent-redetection.
Load the graph and list every bug family
First we load the graph and ask the tool to list every bug family it knows, with nothing chosen ahead of time. It reports all 8 domains and 31 sink constructors and says the census is complete for what the graph can observe. Path containment is just one row here, not something we aimed at.
[lachesis-mcp] loaded the graph; overlay: 0 derived edges; dataflow tier: on demand, per cone
CANDIDATE_CENSUS
move: candidate_census
taxonomy (8):
domain=lifecycle title=Resource lifecycle enumerable=True
domain=memory title=Memory safety enumerable=True
domain=injection title=Injection enumerable=True
domain=navigation title=Request forgery & redirection enumerable=True
domain=object-integrity title=Object integrity enumerable=True
domain=filesystem title=Filesystem enumerable=True
domain=crypto-config title=Cryptography & transport config enumerable=True
domain=resource title=Resource exhaustion enumerable=True
constructors (31): complete_for_observable_graph=True
applied: True
role_nodes: {sink: 21, source: 15}A small line with a large consequence.
Containment is validated only when follow_symlinks is off. With it on, the resolved path is never checked to stay inside the served directory.
if not self._follow_symlinks:
filepath.relative_to(self._directory)// always validate the resolved path against the served directory (fixed in 3.9.2, commit 1c335944).
The original record.
Read the historical advisory and vulnerability record behind this reconstruction.