Case Study: One SSRF, One Metadata Endpoint, One Cloud Account
- Home
- Blog
I’m Joe — a Tokyo-based cybersecurity expert. This is a walkthrough of a real engagement, with the client and product details removed and the technical shape kept intact. It is here because it is the most common serious finding I still see in modern cloud applications, and because almost every team I show it to says the same thing first: “but that endpoint is internal.” That is exactly the problem. Internal is not a security control when your own server is the one making the request.
-
The Feature That Opened the Door
The application let users import a profile picture by pasting a URL. The server fetched that URL and stored the result. That is it — no file upload, no parser, nothing that looks dangerous on an architecture diagram. But “the server fetches a URL the user controls” is the definition of server-side request forgery, and the server sits inside a cloud VPC with a lot of neighbours a browser could never reach. The first thing I test on any feature like this is not the internet — it is what the server can see that the user cannot.
-
Why the Allow-List Did Not Hold
The team had thought about this. There was a blocklist for 127.0.0.1, localhost and 10.0.0.0/8. It failed for the usual reasons, and I walked through them one at a time: a hostname that resolves to an internal address passes a string check; a public URL that answers with a 302 to an internal one is followed without re-validation; IPv6 forms — loopback, unique-local, link-local, IPv4-mapped, NAT64 — were never in the list at all; and a userinfo prefix (http://trusted.example.com@169.254.169.254/) fools any check that parses the host by hand. Validating a string is not validating a destination.
-
From the Metadata Endpoint to Real Credentials
The payload that mattered was boring: the cloud instance metadata service at 169.254.169.254. On this host it still answered version-1 requests, which means a plain GET with no token returns the IAM role attached to the instance and then its temporary access key, secret and session token. I did not need a shell, a callback or a single byte of malware. I needed one HTTP request that the application was designed to make on my behalf, and the response came back rendered in the app’s own error message.
-
Measuring the Blast Radius Before Reporting It
A finding is only as useful as the impact you can prove, and proving impact must never mean causing it. With the client’s written approval I used the recovered credentials for read-only calls: who am I, what policies are attached, what buckets can this role list. The role turned out to be far broader than the feature needed — it could read the object store holding customer documents. I captured the identity call, the policy listing and a single directory listing as evidence, then stopped. No data was downloaded, nothing was modified, and the whole sequence was timestamped so the blue team could match it against their logs.
-
The Fixes That Actually Closed It
Four changes, in the order they were deployed. Enforce IMDSv2 so the metadata service requires a session token that an SSRF cannot mint, and set the hop limit to 1. Re-scope the instance role to exactly the two actions the feature needs. Add an egress policy so the fetcher can only reach the public internet, never the link-local or private ranges. And finally, validate at resolve time rather than parse time: resolve the hostname, check every returned address against a deny-list of private and special-use ranges in both IPv4 and IPv6, then connect to that resolved address so DNS cannot change its answer between the check and the request.
-
Why I Turned This Into a Library
I kept writing the same URL-validation code on engagement after engagement, and I kept watching teams get it subtly wrong — usually on IPv6 or on redirects. So I packaged it: DSSRF, a small MIT-licensed JavaScript library that validates and sanitizes a URL before your client makes the request. It is now listed by the OWASP Foundation under Free for Open Source Application Security Tools, and it has its own published CVEs, because I keep attacking my own library and fixing what I find. That is the point — a defense tool nobody attacks is just a comfortable assumption.
-
What I Would Check in Your Stack Tomorrow
Inventory every place your backend fetches a user-supplied URL: avatar imports, webhooks, PDF and screenshot renderers, link previews, RSS ingestion, SSO metadata, health checks. For each one ask three questions. Can it reach 169.254.169.254 or any private range? Does it follow redirects without re-validating? Does the identity it runs as have more permission than the feature needs? If the answer to any of those is yes, you have the same case study waiting to happen — and it will not stay theoretical for long.
Comments (5)
-
cloud_arch_maya 2 days ago ReplyWe enforced IMDSv2 last quarter after reading something similar and it took an afternoon. The hop-limit detail is the part people miss.
-
Same here. The hard part was finding every fetcher, not the fix itself. Our link-preview service was the one nobody remembered.
-
-
The userinfo prefix trick got us in a code review last month. Our regex looked completely reasonable until someone showed it that URL.
-
Appreciate that you stopped at a directory listing. Too many reports "prove impact" by exfiltrating something they should not have touched.
-
Always. Written scope first, read-only calls, timestamps handed to the defenders. Proof should never cost the client anything.
-