SECURITYBUGFOCUS/SIM4N6 Book a Call
← Back to writing
SSRF April 12, 2026 · 9 min read

DNS rebinding in 2026 — why egress allowlists aren't enough

A short field guide to why a TTL-window race against your own DNS resolver beats most 'block private IPs' controls, drawn from cases against MobSF, MindsDB, and a recent GitLab importer.

DNS rebinding is one of those bug classes that keeps showing up in modern stacks because the mitigation everyone reaches for — a list of blocked private CIDR ranges — solves the wrong half of the problem. The attacker isn’t trying to send a request to 127.0.0.1. The attacker is trying to make your resolver tell your code that some attacker-controlled hostname resolves to 127.0.0.1, after the SSRF check has already passed.

This note walks through why that gap exists, where I’ve found it in the wild, and what actually closes it.

The shape of the bug

A standard SSRF defence looks something like this:

def is_safe(url: str) -> bool:
    host = urlparse(url).hostname
    ip = socket.gethostbyname(host)
    if ipaddress.ip_address(ip).is_private:
        return False
    return True

if is_safe(user_url):
    fetch(user_url)

The check resolves the hostname, decides it’s a public address, and approves the URL. Then the fetch function resolves the hostname again — and gets a different answer.

That second resolution is the rebinding window.

Why TTL=0 is a real attack primitive

An attacker who controls a domain can publish DNS records with extremely short TTLs. The first lookup returns a public IP and passes validation. By the time the application performs the actual HTTP request, the resolver — having long since expired the cached answer — asks again and gets 127.0.0.1, 169.254.169.254, or whatever internal address the attacker wants.

This isn’t theoretical. Three of the SSRF CVEs on my disclosure ledger work exactly this way:

If you can split the resolution into two lookups, you can usually win the race.

What actually fixes it

The fix is structural, not list-based. Two patterns that work:

  1. Resolve once, pin the IP. Do the validation against the resolved IP, then make the outbound request to that IP directly with the original Host header. The library doesn’t get a second chance to be lied to.
  2. Use a hardened HTTP client that refuses redirects to internal IPs. safeurl and similar libraries bake the pinning logic in.

If your application is in a Kubernetes pod, the metadata-service problem also lives here — and 169.254.169.254 is just one rebind away from any “we block private IPs” check that doesn’t pin.

The five-minute self-test

Spin up rbndr.us or run singularity against any endpoint that takes a URL from a user. If you get an internal response back, you have the bug. It really is that fast to find — which is why I keep finding it.


If you want this kind of review against your own product, book a call.

SIM4N6 · APRIL 12, 2026 Discuss this with me →