The archive/CVE-2025-25285
octokit/endpoint.jsDisclosed 2025

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.

Historical recordv9.0.5 (affected: >=4.1.0 <9.0.6, and <10.1.3)
WeaknessCWE-1333 · Regular-expression denial of service
ImpactA regular-expression denial of service in endpoint.js, reachable from attacker-controlled input.
Read the reconstruction
01What happened

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.

Separate the record from the run

What was known.
What Lachesis established.

01Historical record

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
then
02Lachesis run
Verified captured run

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.

The reconstruction, in one frame

What Lachesis reconstructed.

One historical repository. Four captured queries. A complete source-to-sink argument.

Lachesis Casefiles · Unbound ComputeVerified runCVE-2025-25285
A crafted request Accept header (options.headers.accept) with a long run of [\w-] characters and no '-preview' suffix.
01Sourcelet headers = Object.assign({}, options.headers);
02Sinkheaders.accept.match(/[\w-]+(?=-preview)/g)
03Missing guardheaders.accept.match(/[\w-]+(?=-preview)/g)
$ lachesis sources_of → reaches → guard_dominance
octokit/endpoint.js · v9.0.5 (affected: >=4.1.0 <9.0.6, and <10.1.3) · vulnerableunboundcompute.com
This was known history. Your code is not.

Lachesis follows the same evidence path through unfamiliar repositories.

The Lachesis reconstruction

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.

Repositoryoctokit/endpoint.js
Revisionv9.0.5 (affected: >=4.1.0 <9.0.6, and <10.1.3) · vulnerable
EngineLachesis

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.

01 · load_graph01 / 05
Lachesis ran
load_graph --path cve-2025-25285.kuzu --profile all && candidate_census

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.

Captured resultCaptured · load_graph
raw captured outputload_graph
{"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]
src/parse.tsline 0
Code location returned for this evidence step.
The repair

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

Before
headers.accept.match(/[\w-]+(?=-preview)/g)
After the patch
// 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.
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart