~/learn / web-vulnerabilities / ssrf
Server-Side Request Forgery (SSRF)
How URL-fetching features get turned against internal networks and cloud metadata, the parser tricks that defeat blocklists, why DNS rebinding beats naive validation, and how allowlists, network egress control, and IMDSv2 contain it.
Server-side request forgery happens when an application fetches a URL that the user influences, and the server can reach places the user cannot. The request originates from inside the trusted network, carrying whatever implicit trust that network grants. A user who could never reach an internal admin panel, a database with an HTTP interface, or a cloud metadata endpoint from the outside can now reach all of them by proxy, through the vulnerable server.
SSRF rose to the top of the risk charts because cloud architectures made the internal network extraordinarily valuable. The 2019 Capital One breach, which exposed data on roughly 100 million people, was an SSRF against an application that let the server fetch the cloud metadata service and hand back temporary credentials. That is the canonical case study for why this bug is treated so seriously.
Where it hides
Any feature that takes a URL, a hostname, or something that becomes one:
- Webhooks and callback URLs.
- “Import from URL,” avatar-by-URL, and link-preview or unfurl features.
- PDF and screenshot generators that render a user-supplied page.
- Document and image processors that follow embedded references.
- XML parsers resolving external entities (XXE is often SSRF in disguise).
- Any server-side HTTP client whose target is built from user input, including the host portion of a parsed URL.
What the attacker reaches
- Loopback and link-local:
127.0.0.1,::1, and169.254.0.0/16. Loopback exposes services bound only to localhost that assume no external caller. Link-local hosts the cloud metadata service. - Private ranges:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16. Internal dashboards, caches like Redis and Memcached, CI servers, and databases, many with no authentication because they are “internal.” - Cloud metadata:
169.254.169.254. On a misconfigured AWS instance using IMDSv1, a plain GET tohttp://169.254.169.254/latest/meta-data/iam/security-credentials/<role>returns temporary IAM credentials. That is the Capital One pattern. - Non-HTTP protocols: if the client follows
file://, it reads local files;gopher://can forge arbitrary TCP payloads to speak to Redis or SMTP;dict://probes services. Allowlisting schemes tohttpandhttpscloses this whole family.
Why blocklists fail: the host is parsed many times
The instinct is to block “bad” hostnames and IPs. This fails because a URL passes through several parsers (your validator, the DNS resolver, the HTTP client, the OS socket layer) and they disagree about what the host is. The attacker targets that disagreement.
- Alternate IP encodings:
127.0.0.1is also2130706433(decimal),0x7f000001(hex),0177.0.0.1(octal), and127.1(short form). A string blocklist that checks for127.0.0.1sees none of these, but the socket layer resolves them all to loopback. The URL inspector flags these normalizations so you can see what the browser or client will actually dial. - The userinfo trick:
http://[email protected]/has host169.254.169.254. Everything before the@is a username, which a quick visual check or a naive “starts with expected.test” test gets wrong. - IPv6 forms:
[::1],[::ffff:127.0.0.1], and[0:0:0:0:0:ffff:7f00:1]all reach loopback. - DNS that resolves inward: an attacker registers
internal.evil.testand points its A record at169.254.169.254or127.0.0.1. Your allowlist of hostnames passes it; the resolver sends the request inside. - Redirects: the submitted URL is an innocent external site that returns a
302tohttp://169.254.169.254/.... If your client follows redirects, validating only the first URL is pointless.
The hardest bypass: DNS rebinding and TOCTOU
Even “resolve the hostname and check the IP is public” has a race. The validator resolves attacker.test to a public IP and approves it. A moment later, when the HTTP client resolves the same name to actually connect, the attacker’s DNS server answers with 169.254.169.254 instead, exploiting a very short TTL. The check and the use resolved the name twice and got different answers. This is a time-of-check to time-of-use flaw specific to SSRF, and it is why validation at the DNS layer is not enough on its own. The robust answer is to resolve once, pin that resolved IP, validate the pinned IP, and connect to that exact IP.
Defense: allowlist, pin, and contain
No single control is sufficient. Layer them.
- Allowlist, do not blocklist. If the feature should only reach a known set of providers, enumerate them. An allowlist of destinations is finite and reviewable; a blocklist of “everything internal” never ends.
- Validate the resolved IP, then connect to it. Resolve the hostname yourself, reject any address in loopback, private, link-local, or other reserved ranges, and then pin and dial that specific IP so DNS cannot change under you. Re-check after every redirect, or disable redirect following entirely.
- Allowlist schemes and ports. Only
httpandhttps, only80and443, nothing else. This removesfile://,gopher://, and port scanning in one step. - Control egress at the network. Put outbound fetchers in a segment whose firewall denies traffic to internal ranges and to
169.254.169.254by default. This is the control that would have blunted Capital One even with the application bug present, because defense in depth means the app bug alone is not enough. - Require IMDSv2 on AWS. IMDSv2 makes the metadata service require a PUT to obtain a session token first, with a low IP TTL on the hop count, which a simple SSRF GET or a reflected fetch cannot satisfy. Enforce it instance-wide and the classic credential theft stops working.
- Return nothing useful. Do not reflect the fetched response body or precise error and timing back to the user. Blind SSRF is far less productive than one that echoes what it found.
Recognizing it in a test
In an authorized engagement, the signals are a parameter that takes a URL and a server that behaves differently for internal targets: a different error for a closed versus open internal port, a timing difference, or in the worst case the actual response reflected back. Testers confirm interaction using a server they control (a Burp Collaborator style out-of-band host) so that even a blind SSRF proves it reached out. Only ever against systems in scope.
Field notes: the payload arsenal
Once you find a parameter that fetches a URL (webhooks, PDF/image-from-URL, link previews, import-from-URL, XML with external entities), the game is reaching somewhere you should not. Cloud metadata is the crown jewel because it hands out credentials:
# AWS IMDSv1 (no header required) - the Capital One class
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# GCP (requires a header, so you need header control or a redirect)
http://metadata.google.internal/computeMetadata/v1/ (Metadata-Flavor: Google)
# Azure
http://169.254.169.254/metadata/instance?api-version=2021-02-01 (Metadata: true)
# Alibaba / DigitalOcean / Oracle all expose similar 169.254.169.254 paths
Beyond metadata, probe http://localhost:<port> and http://127.0.0.1 for admin panels, databases, and internal APIs that trust network position, and read local files with file:///etc/passwd where the fetcher allows it.
Field notes: beating the allowlist and SSRF filters
Filters that block 127.0.0.1 and 169.254.169.254 by string match fall to encoding, because the OS resolver normalizes forms the regex never saw:
| Technique | Example for 127.0.0.1 |
|---|---|
| Decimal IP | http://2130706433/ |
| Octal | http://0177.0.0.1/ |
| Hex | http://0x7f000001/ |
| IPv6 / mapped | http://[::1]/, http://[::ffff:127.0.0.1]/ |
| Shorthand | http://127.1/, http://0/ |
| Userinfo trick | http://[email protected]/ |
| Rare dotted | http://127.0.0.1.nip.io/ (a resolver that returns the embedded IP) |
Two higher-tier bypasses: a redirect where you point the fetcher at a URL you control that 302s to 169.254.169.254 (defeats any check done only on the first URL), and gopher://, which lets you craft raw TCP bytes to talk to Redis, memcached, or an SMTP server and issue real commands, turning SSRF into internal RCE or data theft.
Field notes: DNS rebinding and blind SSRF
When the server validates the hostname’s IP before fetching, DNS rebinding beats the time gap: you serve a domain that answers with a public IP on the validation lookup and 169.254.169.254 (or 127.0.0.1) on the fetch lookup moments later, with a very low TTL. Services like rebind tooling or a custom authoritative DNS do this; it is the TOCTOU bug applied to name resolution.
When nothing comes back in the response (blind SSRF), you confirm reachability with an out-of-band callback: point the parameter at http://<id>.collab.test (Burp Collaborator / interactsh) and watch for the DNS and HTTP hit. A DNS hit but no HTTP hit tells you egress is filtered to resolution only, which still proves the SSRF and sometimes still reaches internal resolvers.
Code review checklist
- Find every place a server-side HTTP client, image loader, or XML or PDF processor takes a host from user input.
- Confirm validation happens on the resolved IP, not the raw string, and that the connection pins that IP.
- Confirm schemes and ports are allowlisted and redirects are disabled or re-validated.
- Confirm the fetcher’s network egress denies internal ranges and the metadata IP.
- On cloud, confirm IMDSv2 is enforced and instance roles hold only the permissions they need, so stolen credentials have a small blast radius.
PortSwigger’s SSRF labs cover the blocklist bypasses, the redirect and DNS tricks, and blind SSRF against a legal target, which is the only place to practice reaching an internal service.