SSRF (Server-Side Request Forgery)
How making the server fetch a URL you choose lets you reach internal systems — and why blacklists of addresses never hold.
- Track
- Web security
- Estimated time
- 25 min
- Level
- Intermediate
Before this lesson
SSRF is when a server fetches a URL that you control. Lots of features do this: a link preview, an avatar imported from a URL, a webhook, a PDF generator that loads images. The request leaves from the server, inside the server’s network — so you can reach things your own browser never could: internal services, admin panels, and the cloud metadata endpoint that hands out the server’s credentials.
What is the vulnerability?
app.post('/fetch', async (req, res) => {
const r = await fetch(req.body.url); // whatever URL the user sent
res.send(await r.text());
});You send https://example.com and it previews a normal page. You send http://127.0.0.1/admin and the server fetches its own internal admin panel and hands you the response. The server is a confused deputy: it has access you do not, and it is using that access on your behalf.
Why does it happen?
The server treats a user-supplied destination as trusted. It never asks “should I be allowed to fetch this?”. The dangerous targets are the ones only reachable from inside: loopback (127.0.0.1), private ranges (10.x, 192.168.x, 172.16–31.x), and the cloud metadata service at 169.254.169.254, which returns temporary credentials for the server’s role.
Why blacklists fail
The instinct is to block localhost and 127.0.0.1. But one address has many spellings:
127.0.0.1 127.1 2130706433 0x7f000001 0177.0.0.1 [::1] localhostA text filter that bans one spelling misses the rest. Even a filter that resolves the address and blocks private ranges can Relying on listing every bad case; it fails the moment one case is forgotten — here, the link-local metadata range. — the link-local 169.254.0.0/16 where the metadata service lives. You walk through both gaps in the medium and hard labs.
How to prevent it
const ALLOW = new Set(['https://images.cdn.example', 'https://docs.example']);
app.post('/fetch', async (req, res) => {
const url = new URL(req.body.url);
if (!ALLOW.has(url.origin)) return res.status(400).end(); // allowlist, not blacklist
const r = await fetch(url, { redirect: 'error' }); // no redirect to internal
res.send(await r.text());
});Allowlist the destinations the feature actually needs — do not try to list every bad address. Resolve the hostname and reject private, loopback and link-local IPs (check after resolution, and again after any redirect). Isolate the fetching service in a network segment that cannot reach internal systems, require IMDSv2 for cloud metadata, and give the server the least privilege possible so leaked credentials do limited harm.
Labs
The server fetches any URL
SolvedEasyBrowser lab
Make the server fetch its internal admin panel on its local address and get the master stash code.
Enter the labLockedA filter that matches text
SolvedMediumBrowser lab
Reach the internal admin panel again, bypassing the text filter.
Enter the labLockedThe address everyone forgets to block
SolvedHardBrowser lab
Steal the Ravens’ cloud access keys from the metadata service, through the server.
Enter the labLocked
