CVE

CVE-2026-34207: Typebot's SSRF Protection Bypass via DNS-Resolved Hostnames in Webhook / HTTP Request Validation

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.

date: 2026-06-23
CONTENTS

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
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.

Typebot GitHub repository

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.