TL;DR
Server-Side Request Forgery makes the server issue an HTTP request on the attacker’s behalf. The request originates from inside the trust boundary, so it reaches things the attacker cannot: link-local metadata endpoints, internal admin panels, databases bound to loopback. The bypasses that defeat naive filters are the same URL parsing inconsistencies that defeat open redirect allowlists. The fix is to validate the resolved destination IP, not the string.
The primitive
Any feature that fetches a URL supplied by the client is a candidate. Webhooks, PDF and image renderers, URL preview cards, “import from URL”, XML parsers resolving external entities, avatar-by-URL uploads.
$url = $_GET['url'];
$data = file_get_contents($url); // the server fetches whatever the client names
echo $data;
Nothing here checks where $url points. Supply an internal address and the server fetches it for you.
GET /fetch?url=http://127.0.0.1:8080/admin HTTP/1.1
Host: app.example.com
The application server sits behind the firewall. 127.0.0.1, 10.0.0.0/8, 169.254.0.0/16, and internal DNS names are all reachable from where it runs. They are not reachable from the internet. That asymmetry is the whole vulnerability. It is CWE-918, and it is A10 in the OWASP Top 10 for 2021.
The high-value target: cloud metadata
Every major cloud exposes an instance metadata service on the link-local address 169.254.169.254. It hands out instance configuration and, on many setups, temporary credentials for the attached IAM role. An SSRF that reaches it turns into credential theft.
| Provider | Endpoint | Required header |
|---|---|---|
| AWS | http://169.254.169.254/latest/meta-data/ |
none (IMDSv1) |
| GCP | http://metadata.google.internal/computeMetadata/v1/ |
Metadata-Flavor: Google |
| Azure | http://169.254.169.254/metadata/instance?api-version=2021-02-01 |
Metadata: true |
On AWS IMDSv1 there is no header requirement, so a plain GET is enough:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
The response is a set of temporary AccessKeyId, SecretAccessKey, and Token values scoped to the instance role. This is not theoretical. In the 2019 Capital One breach, an attacker used an SSRF against a misconfigured web application firewall to reach the metadata endpoint, retrieved the role credentials, and used them to read storage buckets holding roughly 100 million records. The conviction is documented in the Department of Justice release.
Defeating the allowlist
The first fix a team reaches for is a check on the hostname. Most of these checks compare strings, and string checks on URLs fail the same way they fail for open redirects: they compare bytes when they needed to parse structure. This is the same root cause covered in substring matching is not validation.
Consider a filter that pulls the host and requires it to end with the corporate domain:
$host = parse_url($url, PHP_URL_HOST);
if (str_ends_with($host, 'example.com')) {
$data = file_get_contents($url); // "trusted"
}
Several classes of input walk straight through it.
Alternate IP encodings. 127.0.0.1 has many spellings, and different parsers accept different ones:
http://2130706433/ decimal
http://0177.0.0.1/ octal
http://0x7f000001/ hex
http://[::1]/ IPv6 loopback
http://[::ffff:127.0.0.1]/ IPv4-mapped IPv6
Attacker-controlled DNS. Register a name that resolves to an internal address. http://169.254.169.254.nip.io/ resolves to 169.254.169.254 through wildcard DNS, and the host string still does not contain a private IP for a byte-level check to catch.
Userinfo confusion. A validator that treats example.com as present in http://[email protected]/ reads the userinfo, not the authority. The HTTP client connects to 169.254.169.254.
Parser disagreement. The class Orange Tsai catalogued in A New Era of SSRF: the library that validates the URL and the library that fetches it disagree about which host is the authority. Payloads built from spaces, #, \, and stray @ characters get one host past the check and a different host to the socket:
http://expected.com#@169.254.169.254/
http://169.254.169.254\@expected.com/
If the validator and the client are different code, assume they parse differently until proven otherwise.
Time-of-check to time-of-use
Even a correct DNS resolution can be defeated. The validator resolves attacker.com to a public IP and approves it. Between that lookup and the fetch, the attacker’s DNS server answers the second query with 169.254.169.254. The check passed on the first answer; the request used the second. DNS rebinding turns a valid allowlist entry into an internal fetch. Validating the name is not enough; the resolved IP at connect time is what matters.
Protocol smuggling
SSRF is not limited to http. When the fetch library honors other schemes, the reach widens.
gopher://127.0.0.1:6379/_<url-encoded Redis commands>
file:///etc/passwd
dict://127.0.0.1:11211/stats
gopher:// sends arbitrary bytes over a raw TCP connection, which is enough to script a line-based protocol like Redis or an SMTP conversation. file:// reads local files. Any scheme the client understands is an attack surface, so the scheme itself must be on an allowlist, not a denylist.
The fix
No single control is sufficient. Layer them.
- Allowlist destinations by resolved IP, not by string. Resolve the hostname, reject any answer in a private, loopback, or link-local range (
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1,fc00::/7), then connect to that same address. Pin the resolved IP through to the socket so a second lookup cannot change the answer. - Allowlist the scheme. Permit
httpandhttpsonly. Rejectgopher,file,dict,ftp, and everything else. - Do not follow redirects blindly. A
302tohttp://169.254.169.254/re-introduces the whole problem after validation passed. Re-validate every hop, or disable following. - Require IMDSv2 and set the hop limit to 1. The session-oriented metadata service requires a
PUTto obtain a token before any read. A simple GET-only SSRF cannot mint that token, and the default hop limit of 1 stops requests forwarded through an application layer. This is the control that would have blunted the metadata step in the breach above. - Enforce egress network policy. The application does not need to reach
169.254.169.254or the internal subnet. Block it at the network layer so a code-level miss is not fatal.
The OWASP SSRF prevention cheat sheet and PortSwigger’s SSRF material go deeper on each. The short version is the same rule that governs redirects and origins: parse the input into structure, decide on the resolved value, and never trust a string comparison to tell you where a URL points.