Juicy Shop
Server-Side Request Forgery (SSRF)
SSRF turns your server into a confused deputy, making requests to wherever an attacker chooses — including internal services and cloud metadata. Learn the attack and how allow-lists and network controls stop it.
The confused deputy
Whenever your server fetches a URL supplied (directly or indirectly) by a user — a webhook, an image importer, a PDF renderer, a link previewer — an attacker may be able to choose the destination. The server has network access and trust the attacker lacks, so it becomes a 'confused deputy' that fetches internal resources on the attacker's behalf and returns the result.
Why internal is the prize
From the public internet an attacker cannot reach http://localhost, 169.254.169.254 (cloud metadata), or internal-only admin panels and databases. Via SSRF they can: read cloud instance metadata to steal credentials, scan the internal network, hit unauthenticated internal APIs, and reach services that assume 'anything on this network is trusted'. SSRF is a pivot into the soft interior.
Why naive blocklists fail
Blocking '127.0.0.1' or 'localhost' is easily bypassed: alternate encodings (0x7f.0.0.1, 2130706433), DNS names that resolve to internal IPs, redirects from an allowed host to an internal one, and IPv6 forms. Validation must happen against the RESOLVED IP after DNS resolution, and you must re-validate on every redirect — not just the original string.
Defending against SSRF
Prefer an allow-list of exact destinations the feature legitimately needs. Resolve the hostname, reject private/loopback/link-local/metadata ranges, and pin the connection to the validated IP so a later DNS rebind cannot swap it. Disable unneeded URL schemes (file:, gopher:). Layer network controls: put the fetcher in a segment that simply has no route to metadata or internal services, and block egress by default.
Insecure vs secure
app.post("/import", async (req, res) => {
// Attacker controls url
const r = await fetch(req.body.url);
res.send(await r.text());
});
// fetch("http://169.254.169.254/latest/meta-data/...")The server fetches any attacker-chosen URL, so it can be pointed at cloud metadata or internal services and return their contents.
const ALLOWED = new Set(["images.example.com"]);
app.post("/import", async (req, res) => {
const url = new URL(req.body.url);
if (url.protocol !== "https:" || !ALLOWED.has(url.hostname))
return res.status(400).end();
const ip = await resolveAndRejectPrivate(url.hostname);
const r = await fetchPinned(url, ip); // pinned to validated IP
res.send(await r.text());
});Only an allow-listed https host is permitted, the resolved IP is checked against private ranges, and the connection is pinned to that IP to defeat DNS rebinding.
Key takeaways
- SSRF makes your trusted server fetch attacker-chosen destinations.
- Internal targets (cloud metadata, admin APIs) are the main prize.
- Validate the resolved IP — and re-check on redirects — not just the URL string.
- Prefer allow-lists, reject private/metadata ranges, and pin to the validated IP.
- Add network segmentation and default-deny egress as defence in depth.
Knowledge check
Practice
Apply this lesson hands-on in the matching find-it / fix-it challenge.
Open challenge →