SSRF in the Cloud: Why the Metadata Service Still Matters
A URL-preview feature, an unvalidated fetch, and a link-local address. How server-side request forgery turns into cloud credentials — and how to close every step of the path.
Published by Xevry
Server-side request forgery (SSRF) lets an attacker make your server send HTTP requests to destinations of their choosing. On its own, that can mean port scanning an internal network. In a cloud environment, it can mean something far worse: reaching the instance metadata service and walking away with credentials.
From feature to foothold
SSRF usually starts with a legitimate feature that fetches a user-supplied URL: link previews, webhook delivery, PDF rendering, image import, or "import from URL" buttons.
app.post("/api/preview", async (req, res) => { const page = await fetch(req.body.url); // any URL, any host const html = await page.text(); res.json(extractOpenGraph(html));});On AWS EC2, the instance metadata service (IMDS) lives at the link-local address 169.254.169.254. With the legacy IMDSv1, a single unauthenticated GET to /latest/meta-data/iam/security-credentials/<role-name> returns temporary access keys for the instance's IAM role. If the preview endpoint reflects the response body, the attacker now holds those keys.
What IMDSv2 changes
IMDSv2 requires a session token. The client first sends a PUT to /latest/api/token with the header X-aws-ec2-metadata-token-ttl-seconds, then presents the returned token in an X-aws-ec2-metadata-token header on subsequent requests.
Most SSRF primitives can only issue GET requests and cannot set arbitrary headers, which defeats the attack. IMDSv2 also responds with an IP TTL (hop limit) of 1 by default, so the token response cannot traverse an extra network hop, such as an unconfigured container bridge.
HttpTokens to required on every instance and launch template, and use an account-level default or policy so new instances cannot regress. Other clouds have equivalents — Google Cloud's metadata server, for example, requires a Metadata-Flavor: Google header.Validating URLs correctly
IMDSv2 is defense in depth. The SSRF itself still needs fixing, and URL validation is notoriously easy to get wrong. Common bypasses of naive checks include:
- Alternate IP encodings:
http://2852039166/andhttp://0xA9FEA9FE/both resolve to169.254.169.254. - IPv6 forms, including IPv4-mapped addresses like
::ffff:169.254.169.254. - DNS names that resolve to internal addresses — including names an attacker controls.
- DNS rebinding: the hostname resolves to a public IP during validation, then to an internal IP when the request is actually made.
- Open redirects: a validated public URL that responds with
302to an internal one.
The robust approach is to validate the resolved address you actually connect to, not the string the user supplied:
- Allow only
httpandhttpsschemes, and only expected ports. - Resolve the hostname yourself and reject private, loopback, link-local, and reserved ranges — for both IPv4 and IPv6.
- Connect to the validated IP directly (pinning it for the request) so a second DNS lookup cannot change the target.
- Disable automatic redirects, or re-run validation on every hop.
- Never reflect raw response bodies back to the caller.
Better still, route all user-directed outbound requests through a dedicated egress proxy with its own allowlist, running in a network segment that has no route to internal services or the metadata endpoint.
Reducing blast radius
Finally, assume something will eventually slip through. An instance role scoped to exactly the resources that workload needs turns a credential leak from an account compromise into a contained incident. During infrastructure reviews, we trace this path explicitly: which services fetch URLs, which roles they run as, and what those roles can reach.