
CVE-2026-82286 — gpt-crawler <=1.5.1 unauthenticated arbitrary file write via outputFileName (POST /crawl). PoC + self-contained Docker lab. CVSS 8.6, CWE-22.
Proof-of-concept for an unauthenticated arbitrary file write in
BuilderIO/gpt-crawler (≈22k ★) via the
outputFileName parameter of the POST /crawl API endpoint.
| CVE | CVE-2026-82286 |
| Product | gpt-crawler (BuilderIO) |
| Affected | <= 1.5.1 (latest release; default branch HEAD also affected) |
| Class | Path Traversal / Arbitrary File Write (CWE-22, CWE-73) |
| CVSS 3.1 | 8.6 — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L (HIGH) |
| Auth | None (unauthenticated) |
| Precondition | API server mode running (npm run start:server) |
| Status | CONFIRMED end-to-end (unpatched as of 2026-08-29) |
The API server (src/server.ts) exposes POST /crawl with no authentication. The JSON
body is parsed directly into the crawler Config; outputFileName is validated only as
z.string() — there is no path constraint:
// src/config.ts
outputFileName: z.string(),
In src/core.ts, write() builds the output path straight from that attacker-controlled
string and calls fs.writeFile() on it:
// src/core.ts (write)
const nextFileName = (): string =>
`${config.outputFileName.replace(/\.json$/, "")}-${fileCounter}.json`;
...
await writeFile(nextFileNameString, JSON.stringify(currentResults, null, 2));
A remote, unauthenticated attacker therefore controls the full path of a file written
outside the intended storage/ output area — via an absolute path (/tmp/x,
/home/<user>/x) or ../ traversal. The file content is the crawl result
[{title,url,html}], whose fields come from a page the attacker points the crawler at
(set url to a server you control), so content is attacker-influenced.
The only constraint is the forced -<N>.json suffix (name always ends .json) and the
JSON-array content wrapper — which is why this is scored as arbitrary file write (I:H),
not a clean RCE.
python3 exploit.py \
-t http://TARGET:3000 \
-u http://ATTACKER:8081/index.html \ # page whose content lands in the written file
-o /home/myuser/PWNED # -> writes /home/myuser/PWNED-1.json
The endpoint reflects the written file's content in the HTTP response. Empirical proof is
the file appearing at the attacker-chosen path on the target filesystem (see EVIDENCE.txt).
cd lab && ./run.sh
python3 ../exploit.py -t http://127.0.0.1:3000 -u http://127.0.0.1:8081/index.html -o /tmp/PWNED
docker exec gptc-vuln cat /tmp/PWNED-1.json # <- written outside storage/, unauthenticated
Teardown: docker rm -f gptc-vuln; pkill -f 'http.server 8081'.
An unauthenticated network attacker can create or overwrite files at arbitrary paths
(subject to the process user's permissions and the -<N>.json suffix) with partially
attacker-controlled content. Depending on deployment this enables tampering with
configuration/data files, clobbering application state (integrity/availability), and, on
targets that later consume a written *.json file, can be chained further.
The sink is reachable only in the API server mode (src/server.ts, started via
npm run start:server / the containerapp deployment), not the default CLI mode. The
server binds API_HOST (default localhost); real deployments that expose the API set it
to 0.0.0.0 (as the containerapp image does), making the endpoint remotely reachable.
outputFileName against a fixed base directory and reject the result if it
escapes it (path.resolve(base, name) + prefix check); strip .. and absolute paths.POST /crawl.Look for POST /crawl bodies whose outputFileName contains /, \, or .., and for
*-1.json files appearing outside the crawler's storage/ directory.
Research & PoC: Caio Fabrício (BiiTts).