## https://sploitus.com/exploit?id=E61DF141-B3A8-537B-8845-233051D12F82
# CVE-2026-34207
The SSRF filter checked hostname text, but the actual destination was decided later by DNS. That gap let attacker-controlled Webhook URLs reach loopback, metadata, and private network targets.
## Intro
I found this issue while reviewing **Typebot**, an open-source chatbot builder, with a simple security question in mind:
**What happens if SSRF protection validates hostname text, but the real destination is decided later by DNS?**
In this case, that question led to a real bug.
Typebot's SSRF protection for `Webhook` / HTTP Request blocks validated only:
- the URL string,
- blocked hostname literals,
- and literal IP formats
It did **not** resolve hostnames before allowing the request.
That meant a hostname like `ssrf-repro.example` could look harmless during validation, then resolve to:
- `127.0.0.1`
- `169.254.169.254`
- or RFC1918/private network space
and still be fetched by the backend HTTP client.
That issue became **CVE-2026-34207**.
**Typebot:** [Typebot on GitHub](https://github.com/baptisteArno/typebot.io)
**CVE:** CVE-2026-34207
**Fixed in:** `3.16.0`
This affected **Typebot**, a widely used open-source chatbot platform. On its official site, Typebot is presented as trusted by **650+ companies worldwide**. The site also advertises **2M+ monthly chats** and **1.5M+ published bots**.
---
## Attack Chain
`attacker-controlled Webhook URL -> hostname passes literal-only SSRF validation -> no DNS resolution before allow decision -> backend HTTP client resolves hostname to internal target -> server-side request reaches loopback / metadata / private network -> response data becomes available through execution logs`
---
## What Typebot Does
**Typebot** is a chatbot builder.
It lets users create flows that can:
- ask questions
- collect structured input
- call external services
- chain business logic
- and trigger outbound HTTP requests through `Webhook` / HTTP Request blocks
That means outbound request execution is a real security boundary.
The important question here was not whether Typebot supported Webhook blocks.
The real question was:
**Does the SSRF protection validate the actual destination the server will connect to, or only the hostname text that appears in the URL?**
In this case, it only validated the text form first.
That was the mistake.
---
## Why This Surface Was Worth Looking At
SSRF defenses fail in very predictable ways.
Most of the time, the interesting mistakes are not:
- "you forgot to block `169.254.169.254`"
- or "you forgot to block `localhost`"
The stronger mistakes are boundary mistakes:
- validation happens before canonicalization
- validation happens before redirects
- validation happens before DNS resolution
- validation happens on one representation, but the network stack uses another
That was the right place to look here.
Typebot already had SSRF hardening logic for literal metadata IPs, loopback, private ranges, and encoded IP tricks.
That made the next question obvious:
> **What if the hostname is not literally dangerous, but resolves to a dangerous destination later?**
That is exactly what happened.
---
## Root Cause
The root issue was **destination validation based on hostname text instead of resolved IP address**.
In the vulnerable implementation, `validateHttpReqUrl()`:
- parsed the URL
- allowed only `http:` and `https:`
- blocked a short list of literal hostnames such as `metadata.google.internal`, `metadata.goog`, `metadata`, and `localhost`
- detected literal decimal / hex / octal IP tricks
- parsed literal IPv4 / IPv6 addresses
- validated the address only if the hostname itself was already a literal IP
That is the important part.
If the hostname was a normal value like:
`ssrf-repro.example`
then `parseIPAddress(hostname)` returned `null`, and the validator stopped there.
No DNS resolution happened before approval.
So the vulnerable logic effectively reduced to:
```ts
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
```
That means:
- literal dangerous IPs were blocked
- encoded dangerous IPs were blocked
- but hostnames resolving to dangerous IPs were not
The second half of the bug was in the execution path.
In `executeHttpRequest()`, Typebot first ran validation and then later performed the actual request with `ky(request.url, ...)`.
So the sequence was:
- validate the URL string
- accept the hostname
- later resolve the hostname during the real outbound request
- connect to the resolved internal target
That is the whole vulnerability.
---
## Why This Is a Security Issue, Not Just Incomplete Filtering
The important distinction is **where the trust decision was made**.
A lot of bugs look small if you describe them badly.
If you describe this one as:
> "the hostname filter was incomplete"
it sounds like a quality issue.
That is not the real problem.
The real problem was:
- the server made a security decision before it knew the real destination
- the network stack later connected somewhere else
- and the application treated that request as valid
That is not cosmetic filtering weakness.
That is a trust-boundary failure.
And because the HTTP executor recorded response data in execution logs, the issue was not even blind in the strongest cases.
So this was not just:
- "unexpected internal traffic happened"
It was:
- internal traffic happened
- and the attacker could often recover proof and response content through normal application behavior
That is a real SSRF vulnerability.
---
## Proof of Concept
I used two proof layers because they demonstrated two different things.
### PoC 1: self-contained local resolver demo
The first PoC isolated the root cause cleanly.
I used a small local harness that:
- started a loopback HTTP server on `127.0.0.1`
- validated a URL such as `http://ssrf-repro.example:18080/...`
- used a controlled resolver so `ssrf-repro.example` resolved to `127.0.0.1`
- then performed the request
That demonstrated the exact flaw:
- validation passed because the hostname was not a literal blocked value
- the later request still reached loopback
The captured output showed:
- validator result: passed
- request execution result: loopback reached
- response body returned from the loopback service
That proved the validation gap directly.
---
### PoC 2: real Typebot execution path
The second PoC showed the bug through the actual feature path that matters.
The simplest reproduction was:
1. Map a benign hostname to loopback on the machine where the Typebot backend resolves DNS
2. Start a local HTTP service
3. Create a `Webhook` block pointing to that benign hostname
4. Trigger the block through a normal authenticated preview or live execution path
A representative block looked like this:
```json
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
```
With a hosts-file entry such as:
```text
127.0.0.1 ssrf-repro.example
```
the validator still accepted the URL because:
- `ssrf-repro.example` was not in `blockedHostnames`
- it was not `localhost`
- `parseIPAddress("ssrf-repro.example")` returned `null`
- no destination IP validation occurred
Then the backend HTTP client resolved the hostname to `127.0.0.1` and connected anyway.
That established the full claim:
- the vulnerable validation logic was reachable in real feature use
- the request hit a blocked class of destination
- and the application still treated it as a successful outbound HTTP request
---
## Why The Two PoCs Were Chosen This Way
The first PoC proves root cause.
The second PoC proves product impact.
That split matters.
If you only show:
> "this filter accepts a hostname"
you have not shown enough.
If you only show:
> "an internal request happened"
you have not isolated why.
The stronger report is:
- hostname-based validation accepted a value that should not have been trusted
- DNS resolution later changed the real security meaning of that value
- the backend connected to a blocked destination
- and the feature path still completed
That is the full story.
---
## Severity And Classification
This issue was reasonably classified as **High**.
The advisory classification was:
- **CWE-918**: Server-Side Request Forgery (SSRF)
- **CWE-20**: Improper Input Validation
- **CVSS:**
```text
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
```
That makes sense.
This was not an unauthenticated internet-wide SSRF by itself.
The privileges required were **Low** because the standalone bug required an actor who could configure or trigger a `Webhook` / HTTP Request block.
But within that boundary, the impact was serious:
- loopback access
- private network access
- metadata access
- and response exfiltration through logs
That is a strong SSRF issue on its own.
And it gets even more dangerous when combined with other bugs that broaden reachability.
---
## Why This Was Still Worth Reporting
Some people see authenticated SSRF and immediately underrate it.
That is a mistake.
The real question is not:
> "Was the attacker logged in?"
The real question is:
> "Could that attacker make the server connect somewhere the security model was supposed to forbid?"
Here, the answer was yes.
The validator claimed to protect:
- metadata services
- loopback
- RFC1918 private ranges
- IPv6 local ranges
But a hostname-resolution gap let those same destinations back in through a different representation.
That is exactly the kind of bug that deserves a CVE.
---
## Fix Analysis
The fix was solid because it corrected the real boundary, not just a symptom.
In the patched implementation, `validateHttpReqUrl()` was changed to resolve hostnames before approving the request.
The validator now:
- imports `lookup` from `node:dns/promises`
- treats validation as asynchronous
- resolves non-literal hostnames before allowing them
- parses every resolved address
- validates every resolved address against the same blocked ranges
That is the right fix because it changes the trust decision from:
- "does the hostname text look okay?"
to:
- "does the actual destination resolve to a permitted address?"
That is the security property that should have existed from the start.
The remediation also added regression coverage for:
- hostnames resolving to loopback
- hostnames resolving to RFC1918 space
- allowlisted internal hosts for self-hosters
- preservation of non-bypassable protections for metadata and loopback targets
That is the kind of hardening you want in a real SSRF fix:
- the boundary is corrected
- the intended behavior is documented in code
- and tests lock the class down
The issue was resolved in **Typebot 3.16.0**.
---
## Disclosure
This issue was reported privately through GitHub's security reporting flow.
The report included:
- the root cause in `validateHttpReqUrl()`
- the downstream execution path in `executeHttpRequest()`
- a realistic reproduction strategy using hostname resolution to loopback / private targets
- and a self-contained demo to isolate the validation gap
The maintainers accepted the issue, fixed the validation logic, and the vulnerability was later published as:
**CVE-2026-34207**
with the fix released in **Typebot 3.16.0**.
---
## What This Bug Actually Teaches
The key lesson here is simple:
> security decisions must be made on the same representation the network stack will actually use.
That sounds obvious.
But a lot of SSRF protections fail exactly because they do not follow that rule.
- A hostname string looks safe.
- DNS changes what it means.
- The request still goes out.
That is enough.
This bug also reinforces something important about SSRF review in general:
- literal-IP filtering is not enough
- hostname blacklists are not enough
- encoded-IP checks are not enough
If you do not resolve and validate the real destination, your SSRF filter is still incomplete.
That is the real takeaway.
---
## Key Points
- SSRF defenses fail when validation happens before destination resolution
- hostname-text validation is not the same thing as destination-IP validation
- Webhook / HTTP Request features are real security boundaries
- authenticated SSRF can still be high severity when it reaches metadata and internal services
- a strong report connects root cause and impact, not just one or the other
- the fix was correct because it moved the decision to the actual resolved destination
---
## Final Words
This vulnerability was not about a fancy payload.
It was about asking the right boundary question.
In Typebot, the SSRF validator checked the hostname text first.
The actual destination was decided later by DNS.
The network client followed the resolved address.
That gap was the bug.
That is why this became **CVE-2026-34207**.
Fixed in **Typebot 3.16.0**.