Half the features in a modern app fetch a URL on behalf of a user. Webhook receivers, image proxies, link previews, 'import from URL' buttons, third-party integrations. Every one of them is a small polite helper the server runs. Every one of them will happily run against your own internal network unless you teach it not to.
Server Side Request Forgery is when your server makes an outbound HTTP call on behalf of a user — webhook, preview, image proxy, import-from-URL — and the user gets to choose the destination. The server is inside your network. The user is not. Suddenly the user can reach things only the server was supposed to see.
What your AI actually built
You asked for a webhook endpoint that lets users register a callback URL. Or an image proxy so avatars load through your CDN. Or a link unfurler for pasted URLs. The model gave you a tidy fetch call wrapped in a route.
What it did not give you was the allowlist. The fetch will go anywhere the DNS resolver points — including 169.254.169.254 (cloud metadata), 127.0.0.1 (whatever is bound to loopback), and 10.0.0.0/8 (your internal services). To the server, all three look like perfectly valid URLs.
The really painful version is the redirect trick. You block localhost in the handler, the model says 'great, safe,' and then attacker.com returns a 302 pointing at 169.254.169.254 and the fetch follows it. The check happened once. The network call happened twice.
How it gets exploited
A SaaS app ships a nice feature: paste any URL and we will generate a preview card.
- 1Find the featureThe attacker pastes https://example.com and watches the response — a preview with title and og:image. Clearly a server-side fetch.
- 2Point it inwardThey paste http://169.254.169.254/latest/meta-data/iam/security-credentials/ — the AWS EC2 instance metadata endpoint. The preview card comes back with the name of an IAM role.
- 3Drill downThey paste the full credentials URL. The preview now contains a valid AccessKeyId, SecretAccessKey, and Token — the temporary credentials the EC2 instance uses to talk to S3.
- 4Walk into the bucketThey configure awscli with the stolen token and list buckets. The application's S3 bucket — which holds every user's uploaded document — is now readable from the attacker's laptop.
A link-preview feature became a full AWS credential theft. No password was cracked, no dependency was exploited, no shell was opened. The server was just too polite about which URLs it would fetch.
Vulnerable vs Fixed
// app/api/preview/route.ts
export async function POST(req: Request) {
const { url } = await req.json();
// Whatever URL the user hands us, we go get it.
const res = await fetch(url);
const html = await res.text();
return Response.json({
title: extractTitle(html),
image: extractOgImage(html),
});
}// app/api/preview/route.ts
import { lookup } from 'dns/promises';
import ipaddr from 'ipaddr.js';
async function isPublic(hostname: string) {
const { address } = await lookup(hostname);
const parsed = ipaddr.parse(address);
const range = parsed.range(); // 'private', 'loopback', 'linkLocal', 'unicast'...
return range === 'unicast';
}
export async function POST(req: Request) {
const { url } = await req.json();
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
return Response.json({ error: 'https only' }, { status: 400 });
}
if (!(await isPublic(parsed.hostname))) {
return Response.json({ error: 'internal address' }, { status: 400 });
}
// Resolve once, forbid redirects so the check cannot be bypassed.
const res = await fetch(parsed.toString(), {
redirect: 'error',
signal: AbortSignal.timeout(5_000),
});
const html = await res.text();
return Response.json({
title: extractTitle(html),
image: extractOgImage(html),
});
}Three things matter. Resolve the hostname yourself before the fetch. Reject private, loopback, and link-local ranges. Disable redirect following — otherwise attacker.com can 302 into 169.254.169.254 and the check you did on the original URL is useless.
A real case
Capital One — 100 million records via a single SSRF
In 2019 a misconfigured WAF let an attacker ask an EC2 instance to fetch a URL. That URL was the AWS metadata endpoint. The stolen temporary credentials opened the bucket with 100 million credit applications in it.
Related reading
What we find
ssrfReferences
Find every endpoint that fetches a URL for you.
Flowpatrol probes every webhook, image proxy, and URL importer in your app and confirms which ones follow the user inside your network. Five minutes. One URL.
Try it free