
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.
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.
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:
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.1169.254.169.254and still be fetched by the backend HTTP client.
That issue became CVE-2026-34207.
Typebot: Typebot on GitHub
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.
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
Typebot is a chatbot builder.
It lets users create flows that can:
Webhook / HTTP Request blocksThat 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.
SSRF defenses fail in very predictable ways.
Most of the time, the interesting mistakes are not:
169.254.169.254"localhost"The stronger mistakes are boundary mistakes:
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.
The root issue was destination validation based on hostname text instead of resolved IP address.
In the vulnerable implementation, validateHttpReqUrl():
http: and https:metadata.google.internal, metadata.goog, metadata, and localhostThat 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:
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
That means:
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:
That is the whole vulnerability.
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:
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:
It was:
That is a real SSRF vulnerability.
I used two proof layers because they demonstrated two different things.
The first PoC isolated the root cause cleanly.
I used a small local harness that:
127.0.0.1http://ssrf-repro.example:18080/...ssrf-repro.example resolved to 127.0.0.1That demonstrated the exact flaw:
The captured output showed:
That proved the validation gap directly.
The second PoC showed the bug through the actual feature path that matters.
The simplest reproduction was:
Webhook block pointing to that benign hostnameA representative block looked like this:
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
With a hosts-file entry such as:
127.0.0.1 ssrf-repro.example
the validator still accepted the URL because:
ssrf-repro.example was not in blockedHostnameslocalhostparseIPAddress("ssrf-repro.example") returned nullThen the backend HTTP client resolved the hostname to 127.0.0.1 and connected anyway.
That established the full claim:
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.