Server-Side Request Forgery (SSRF) earned its own slot as A10:2021 in the OWASP Top 10 for a reason: it is one of the few flaws that lets an external attacker make requests from inside your infrastructure, using the trust your servers already have. A single unvalidated URL parameter can become a bridge from the public internet straight to internal services, private APIs, and — most damaging of all — the cloud metadata endpoint that hands out temporary credentials.
What SSRF actually is
SSRF happens whenever an application fetches a remote resource using a URL (or host, or IP) that a user can influence, without validating where that request is allowed to go. The classic sinks are image proxies, webhook validators, URL preview generators, PDF renderers, document importers, and any "import from URL" feature. The application server dutifully performs the request on the attacker's behalf, and because that server sits behind the firewall, it can reach destinations the attacker never could directly.
The canonical mapping is CWE-918: Server-Side Request Forgery. A network-exploitable, unauthenticated SSRF that reads cloud credentials typically scores in the CVSS 9.1–9.9 range — for example AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N, where the Scope: Changed flag reflects the pivot from the web tier into the cloud identity plane.
The attack chain, step by step
Consider an endpoint that generates link previews:
- 1. Discovery. The attacker finds
POST /api/preview {"url":"https://example.com/article"}and notices the response embeds fetched content. They swap inhttp://127.0.0.1:8080/and get an internal admin page back — SSRF confirmed. - 2. Internal recon. They probe
http://169.254.169.254/,http://10.0.0.0/8ranges, and common ports (Redis 6379, Elasticsearch 9200, internal 8500 Consul) to map what the server can reach. - 3. Hit the metadata service. On AWS with the legacy IMDSv1, a plain GET to
http://169.254.169.254/latest/meta-data/iam/security-credentials/returns the attached IAM role name, and appending that name returns a JSON blob withAccessKeyId,SecretAccessKey, and aToken. - 4. Credential theft and lateral movement. The attacker exports those temporary keys, runs
aws sts get-caller-identity, and now acts as the EC2 instance's role — reading S3 buckets, DynamoDB tables, or (if the role is over-privileged) escalating across the account.
The 2019 Capital One breach — over 100 million records — followed exactly this pattern: an SSRF against a misconfigured component reached the metadata endpoint, harvested role credentials, and used them to exfiltrate data from S3.
Why 169.254.169.254 is so dangerous
The 169.254.0.0/16 block is the IPv4 link-local range (RFC 3927). Every major cloud pins its Instance Metadata Service (IMDS) to 169.254.169.254 — AWS, Google Cloud (via metadata.google.internal), and Azure all use it. It is unauthenticated by design: any process on the host, including your web app making an outbound request, can read it. That is fine until an attacker gets to choose the request target.
Bypasses defenders miss
Naive blocklists that just string-match "169.254.169.254" or "localhost" fail against:
- Alternate IP encodings:
http://2852039166/(decimal),http://0xA9FEA9FE/(hex), orhttp://[::ffff:169.254.169.254]/(IPv6-mapped). - DNS rebinding: a hostname that resolves to a safe IP during validation, then to
169.254.169.254on the actual fetch (TOCTOU between check and use). - Redirects: the supplied URL returns a
302to an internal address, which the HTTP client follows. - Alternate schemes:
file://,gopher://, ordict://to reach non-HTTP internal services.
Defenses that actually work
Layer these — no single control is sufficient:
- Enforce IMDSv2 and hop-limit 1. IMDSv2 requires a session token obtained via a
PUTrequest carryingX-aws-ec2-metadata-token-ttl-seconds; SSRF via a plain GET (the common case) cannot mint that token. Set the instance toHttpTokens=requiredandHttpPutResponseHopLimit=1so containers can't reach it. This is the single highest-value fix. - Allowlist, don't blocklist. Validate the destination against an explicit allowlist of schemes (
httpsonly), hosts, and ports. Reject everything else by default. - Resolve then pin. Resolve the hostname to an IP, verify that IP is public (reject private RFC 1918, loopback, link-local 169.254.0.0/16, and IPv6 equivalents), then connect to that exact IP — closing the DNS-rebinding window. Re-validate after every redirect and disable auto-follow where possible.
- Network egress controls. Put the fetching workload in a subnet with a security group / firewall that denies outbound traffic to the metadata IP and internal ranges. Defense in depth for when app-layer validation is bypassed.
- Least-privilege IAM. Assume credentials will leak; scope the instance role to the minimum, and use short TTLs so stolen tokens expire fast.
SSRF is cheap to exploit and catastrophic when it lands on the metadata service. Treat every user-influenced URL as hostile, validate at the resolved-IP level, and turn on IMDSv2 today — it is a free config change that neutralizes the most common exploitation path.