
CVE-2026-82286 — gpt-crawler <=1.5.1 escritura arbitraria de archivos sin autenticación mediante outputFileName (POST /crawl). PoC + laboratorio Docker autónomo. CVSS 8.6, CWE-22.
Prueba de concepto para una escritura arbitraria de archivos sin autenticación en
BuilderIO/gpt-crawler (≈22k ★) a través del
parámetro outputFileName del endpoint de API POST /crawl.
| CVE | CVE-2026-82286 |
| Producto | gpt-crawler (BuilderIO) |
| Afectado | <= 1.5.1 (última versión; la HEAD de la rama predeterminada también está afectada) |
| Clase | Path Traversal / Escritura Arbitraria de Archivos (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 (ALTA) |
| Autenticación | Ninguna (sin autenticación) |
| Precondición | Modo servidor API en ejecución (npm run start:server) |
| Estado | CONFIRMADO de extremo a extremo (sin parche al 2026-08-29) |
El servidor API (src/server.ts) expone POST /crawl sin autenticación. El cuerpo
JSON se analiza directamente en la Config del crawler; outputFileName se valida solo como
z.string() — no hay restricción de ruta:
// src/config.ts
outputFileName: z.string(),
En src/core.ts, write() construye la ruta de salida directamente a partir de esa cadena
controlada por el atacante y llama a fs.writeFile() sobre ella:
// src/core.ts (write)
const nextFileName = (): string =>
`${config.outputFileName.replace(/\.json$/, "")}-${fileCounter}.json`;
...
await writeFile(nextFileNameString, JSON.stringify(currentResults, null, 2));
Por lo tanto, un atacante remoto sin autenticación controla la ruta completa de un archivo escrito
fuera del área de salida prevista storage/ — mediante una ruta absoluta (/tmp/x,
/home/<usuario>/x) o un traversal ../. El contenido del archivo es el resultado del rastreo
[{title,url,html}], cuyos campos provienen de una página a la que el atacante apunta el crawler
(establece url a un servidor que controles), por lo que el contenido está influenciado por el atacante.
La única restricción es el sufijo forzado -<N>.json (el nombre siempre termina en .json) y el
envoltorio de contenido de matriz JSON — por eso esto se puntúa como escritura arbitraria de archivos (I:H),
no como un RCE limpio.
python3 exploit.py \
-t http://TARGET:3000 \
-u http://ATTACKER:8081/index.html \ # página cuyo contenido termina en el archivo escrito
-o /home/myuser/PWNED # -> escribe /home/myuser/PWNED-1.json
El endpoint refleja el contenido del archivo escrito en la respuesta HTTP. La prueba empírica es
el archivo que aparece en la ruta elegida por el atacante en el sistema de archivos del objetivo (ver 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 # <- escrito fuera de storage/, sin autenticación
Limpieza: docker rm -f gptc-vuln; pkill -f 'http.server 8081'.
Un atacante de red sin autenticación puede crear o sobrescribir archivos en rutas arbitrarias
(sujeto a los permisos del usuario del proceso y al sufijo -<N>.json) con contenido parcialmente
controlado por el atacante. Dependiendo del despliegue, esto permite manipular archivos de
configuración/datos, sobrescribir el estado de la aplicación (integridad/disponibilidad) y, en
objetivos que posteriormente consumen un archivo *.json escrito, puede encadenarse más allá.
El sumidero solo es alcanzable en el modo servidor API (src/server.ts, iniciado mediante
npm run start:server / el despliegue containerapp), no en el modo CLI predeterminado. El
servidor vincula API_HOST (predeterminado localhost); los despliegues reales que exponen la API lo establecen
en 0.0.0.0 (como hace la imagen containerapp), lo que hace que el endpoint sea remotamente alcanzable.
outputFileName contra un directorio base fijo y rechaza el resultado si
escapa de él (path.resolve(base, name) + verificación de prefijo); elimina .. y rutas absolutas.POST /crawl.Busca cuerpos de POST /crawl cuyo outputFileName contenga /, \ o .., y archivos
*-1.json que aparezcan fuera del directorio storage/ del crawler.
Investigación y PoC: Caio Fabrício (BiiTts).