CVE-2025-25285:endpoint.js: Regular-expression denial of service
A reconstruction of the flaw, and the evidence path Lachesis follows to make it visible.
A crafted request Accept header (options.headers.accept) with a long run of [\w-] characters and no '-preview' suffix.
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-2025-25285
- Vulnerable target
- v9.0.5 (affected: >=4.1.0 <9.0.6, and <10.1.3) · vulnerable
- Prior knowledge
- CVE-2025-25285 · v9.0.5 (affected: >=4.1.0 <9.0.6, and <10.1.3) is vulnerable
Independent re-detection
The experiment graph was built around the vulnerable release (v9.0.5, src/parse.ts:12-107), enriched (pass2 dataflow + atropos bind + pass3 semantic), then enumerated BLIND over the whole constructor registry -- no family hard-coded, no target line seeded. This CVE was an out-of-catalogue MISS in the first campaign: the only redos model was tainted-PATTERN-into-RegExp, and this bug is the DUAL shape (static catastrophic regex literal + attacker-controlled SUBJECT via String.match). The catalogue since gained a receiver/subject-tainted regex model (typescript.regex.match.recv), so the line-62 sink now binds and the blind enumeration surfaces it on its own. This is the ReDoS analogue of the rsync memory.index.capacity unblock the earlier MISS write-up predicted. Prose in `adjudication`/`trace` is MY reading of the verbatim source + official fix, separated from tool output.
- Seeded inputs
- None
- Run timestamp
- 2026-08-29T00:00:00Z
- Evidence artifact
- cve-2025-25285.kuzu (built from @octokit/endpoint.js v9.0.5, enriched pass2+pass3; graph + sidecars deleted after capture)
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 runA real replay of Lachesis over the graph we built for this case, on the upgraded catalogue that models the subject-tainted regex-match sink. Every `output` below is verbatim captured tool text; the graph was built bounded (hard memory/time cap) and released immediately after capture, so nothing accumulates.
Load the graph and list every bug family
Load the graph built from the vulnerable release and list every bug family the blind enumerator knows -- no family is pre-selected.
{"move": "load_graph", "graph": "/Users/riyandhiman/.claude/jobs/12d61190/tmp/hr/cve-2025-25285.kuzu", "profile": "all", "nodes": 2565}
CANDIDATE_CENSUS
move: candidate_census
taxonomy (8):
domain=lifecycle title=Resource lifecycle meaning=an operation that acquires, releases, uses, or transfers a tracked resource obligation=resource operations stay within the object's lifetime primary=True languages=['c', 'python', 'javascript', 'typescript'] enumerable=True families=[{'family': 'acquire', 'constructor': 'lifecycle.acquire'}, ... , {'family': 'regex', 'obligation': 'a regex cannot be driven to catastrophic backtracking', 'constructor': 'resource.regex.complexity', 'enumerable': True}]
... [truncated]A small line with a large consequence.
the static regex is unanchored on the left; no linear-time formulation and no length bound on the attacker-controlled subject, so it admits super-linear backtracking on adversarial input
headers.accept.match(/[\w-]+(?=-preview)/g)
// 9.0.6/10.1.3: /(?<![\w-])[\w-]+(?=-preview)/g -- the negative lookbehind gives the run a left anchor so each starting position is tried once.
The original record.
Read the historical advisory and vulnerability record behind this reconstruction.