The shape of it
Your server is inside the network. It can reach the database, the admin panel on port 8080, the other services, and — on every major cloud — a metadata endpoint that hands out credentials to anything that asks from inside the machine.
So an attacker who can choose the URL is making requests as your server, from a place they could never reach themselves. That is server-side request forgery, and it was the vector in some of the largest cloud breaches on record.
// "paste a link and we'll grab the preview image"
app.post("/preview", async (req, res) => {
const page = await fetch(req.body.url); // whatever they typed
res.send(extractImage(await page.text()));
});
What they will try
http://169.254.169.254/latest/meta-data/ # cloud credentials
http://localhost:8080/admin # a service with no auth, "because it is internal"
http://10.0.0.5/ # anything on the private network
http://[::1]:6379/ # redis, over IPv6
file:///etc/passwd # not even HTTP
http://2130706433/ # 127.0.0.1 as a decimal integer
http://127.0.0.1.nip.io/ # a public name that resolves to 127.0.0.1
http://evil.test/redirect → http://169.254.169.254/ # the redirect does the work
The defence, in order
- Allowlist the scheme —
httpandhttps, nothing else.file:,gopher:andftp:have no business here. - Allowlist the host if you can. "Fetch from these five partners" is a solved problem; "fetch anything on the internet" is the hard version.
- Resolve the name yourself, then check the address. Refuse loopback, private ranges (10/8, 172.16/12, 192.168/16), link-local (169.254/16, and the IPv6 equivalents), and anything else not globally routable.
- Do not follow redirects — or follow them one at a time, re-checking the address at every hop. This is the step most implementations miss.
- Connect to the address you checked, not to the name again: between the check and the connection, DNS can change its answer. That race has a name, DNS rebinding.
And make the blast radius small
Do the fetching from somewhere that cannot reach anything interesting: a separate egress proxy, a container with no route to the private network, a serverless function in its own VPC with an allowlist. Then a missed check is a request to the internet rather than a request to your database.
On AWS, require IMDSv2 — it needs a PUT with a header, which a blind SSRF cannot do. That one setting removes the classic credential theft.
Not only fetch
Anything that takes a location from a user is the same bug wearing different clothes: a webhook URL, an XML parser that resolves external entities (XXE), a PDF renderer that loads images, a markdown preview that fetches a remote image, a "test this connection" button in an admin panel. Ask the same three questions: which schemes, which hosts, which addresses.