August 30, 2026
π How I Bypassed an SSRF Filter Using an IPv6 Address
How an βunallowedβ localhost became reachable just by writing it differently π΅οΈββοΈ
By Chavanamit
3 min read
π A quick word before we dive in β I'm Amit, a security researcher who spends most of his free time chasing bugs on bounty programs and writing about the ones worth telling.
A few days back I was poking at a "Security Scan" tool on a program I'll call doomscan.io (not a real target, just riding out the countdown to doomsday)β paste a URL, hit scan, get info back. That means one thing under the hood: the server fetches a URL you give it. Server-side URL fetch = my brain immediately goes to SSRF.
So I opened Burp and started poking.
π― The endpoint
POST /api/scan/fetch HTTP/2
Host: scan.doomscan.io
Content-Type: application/json; charset=UTF-8
"https://example.com"POST /api/scan/fetch HTTP/2
Host: scan.doomscan.io
Content-Type: application/json; charset=UTF-8
"https://example.com"Send it a JSON string URL, get back the response headers it fetched. No auth. No CAPTCHA.
π« Round 1: the obvious stuff β all blocked
"http://127.0.0.1" β "URL host is not allowed""http://127.0.0.1" β "URL host is not allowed"
Same story for 169.254.169.254, RFC1918 ranges, decimal-encoded IPs, and any redirect ("Test blocked by server"). Whoever wrote this filter had actually read an SSRF cheat sheet β decimal trick covered, private ranges covered, even redirects killed so I couldn't pivot through an open redirect. Annoyingly solid.
π Round 2: googling for the overlooked thing
I re-read some SSRF bypass notes and landed on something I knew about but had never actually fired in the wild: IPv4-mapped IPv6 addresses.
The short version: IPv6 has a reserved block, ::ffff:0:0/96, that lets you write an IPv4 address inside an IPv6 one. So 127.0.0.1 can also be written ::ffff:127.0.0.1 β same destination, different spelling, and a totally legitimate part of the spec, not some obscure trick.
Most SSRF blocklists are just string comparisons: if host in ["127.0.0.1", "169.254.169.254", ...]: reject(). That check has zero idea ::ffff:127.0.0.1 is the same address in a costume. The filter shrugs and lets it through β then curl unwraps it back to 127.0.0.1 and connects anyway.
The filter and the thing doing the actual connecting weren't looking at the address the same way. That gap is the whole bug.
β Round 3: testing it
"http://[::ffff:127.0.0.1]" β success: true"http://[::ffff:127.0.0.1]" β success: true
Reread that response twice. Same address I'd been blocked from fifteen minutes earlier, spelled differently, walked right in.
π³οΈ Round 4: okay, but what's actually reachable?
A success: true on loopback alone doesn't get you paid. So I walked ports on the loopback interface and read the shape of each failure since the endpoint only returns headers, never a response body:
:22 β "Received HTTP/0.9 when not allowed" β OPEN (SSH banner confused curl's HTTP parser):22 β "Received HTTP/0.9 when not allowed" β OPEN (SSH banner confused curl's HTTP parser)
:3306 β "Failed to connect... Couldn't connect" β CLOSED
:6379 β "Empty reply from server" β OPEN, non-HTTP service:3306 β "Failed to connect... Couldn't connect" β CLOSED
:6379 β "Empty reply from server" β OPEN, non-HTTP service
Three different error strings, three different facts about the internal network β without reading a single byte of response body. An unauthenticated attacker could methodically map the server's internal network this way: the exact recon an internal attacker does post-breach, except this one never had to breach anything.
π‘ Takeaway
Not a zero-day. A well-documented bypass I found by googling, not discovering. That's kind of the point β you don't need a novel technique, you need to keep trying known ones after the first five get blocked. The gap between "what the filter checks" and "what the network layer actually connects to" is one of the oldest patterns in SSRF hunting, and it's still showing up in 2026. If you're hunting SSRF, IPv4-mapped IPv6 should just be reflex, right next to decimal and octal encoding.
π That's a wrap! thanks for sticking around till the end. If this saved you five minutes of staring at a 403 on your next SSRF hunt, it did its job.