SSRF Explained: How Server-Side Request Forgery Works

What is SSRF? Server-Side Request Forgery is a vulnerability where an attacker gets a server to make an HTTP request to a destination of their choosing instead of the one the application intended. The request goes out carrying the server's identity, network position, and credentials, reaching places the attacker could never hit directly from the internet.

SSRF is common enough that OWASP gave it a dedicated slot in the Top 10 2021 (A10), pulled out of the general injection category after community surveys flagged how often it showed up and how badly it tends to go. The category exists because SSRF isn't a niche edge case. It's a predictable outcome of a very common feature.

What does a typical SSRF bug look like? #

Almost every SSRF report traces back to the same shape of feature: something on the server accepts a URL from the user and fetches it. Link preview generators, webhook configuration, PDF renderers that load a page, image proxies, "import from URL" buttons, SSO metadata fetchers. Each one is a server voluntarily making outbound requests based on user input, which is exactly the primitive an attacker needs.

The classic version: a user submits a URL, the backend fetches it to generate a thumbnail or preview, and returns something derived from the response. Instead of pointing that fetcher at a normal website, the attacker points it at http://169.254.169.254/latest/meta-data/iam/security-credentials/. That address is the instance metadata service on AWS, and equivalents exist on GCP and Azure. It's only reachable from inside the instance, which is why the server can reach it and the attacker's own browser cannot. The response can include temporary IAM credentials tied to the instance's role, and from there the attacker doesn't need to breach anything else. They just use the stolen keys.

Why is the cloud metadata endpoint the classic SSRF target? #

Because it turns a single vulnerable fetch into full cloud account access, and it's the same address on every instance, so no reconnaissance is needed. The 2019 Capital One breach is the case most people cite: an SSRF vulnerability in a web application firewall configuration allowed a request to the metadata endpoint, which returned credentials for a role with broad S3 access, leading to the exposure of over 100 million customer records. It's the reference example for a reason: it shows the whole chain in one incident, from "server fetches a URL" to "attacker has your cloud account."

What else can SSRF reach besides metadata? #

The metadata endpoint gets the headlines, but it's one target among several. Once a server will fetch an arbitrary destination on the attacker's behalf, it becomes a proxy into everything that server can see:

Internal-only services. Admin panels, internal APIs, databases, and monitoring dashboards that were never meant to be internet-facing are often reachable from inside the network with no authentication, because the assumption was "you'd have to already be inside to hit this."

Localhost. Services bound to 127.0.0.1 on the same host, expecting only local processes to talk to them, are fair game once the server is doing the fetching.

Other hosts on the private network. SSRF lets an attacker probe the internal 10.x, 172.16.x, and 192.168.x ranges the way they'd port-scan a public network, routed through the compromised server's network position.

Basic SSRF versus blind SSRF #

In basic (or "in-band") SSRF, the application returns the response from the forged request, so the attacker reads the metadata, the internal page, or the error message directly. That's the easy case to demonstrate and the easy case to find.

Blind SSRF is quieter. The application makes the request but never shows the attacker the response body. The attacker has to infer what happened from side effects: how long the request took (a fast failure suggests a closed port, a long hang suggests something is listening and not responding), whether a follow-up webhook or callback fired, or whether an out-of-band interaction platform logged a DNS lookup or HTTP hit from the target server. Blind SSRF still enables real damage, including triggering actions on internal services that don't need to send data back to be useful, like a POST to an internal admin endpoint.

How do you defend against SSRF? #

Vulnerable: fetch whatever URL the user hands over.

javascript.javascript

app.post('/preview', async (req, res) => {
const { url } = req.body;
// No validation at all — any scheme, any host, including
// internal IPs and the cloud metadata address.
const response = await fetch(url);
const body = await response.text();
res.send(extractPreview(body));
});

Better: allowlist the destination and block private ranges.

javascript.javascript

const ALLOWED_HOSTS = new Set(['images.example.com', 'cdn.example.com']);

function isPrivateOrLinkLocal(ip) {
return (
ip.startsWith('127.') ||
ip.startsWith('10.') ||
ip.startsWith('192.168.') ||
ip.startsWith('169.254.') || // covers the metadata address
/^172\.(1[6-9]|2\d|3[01])\./.test(ip)
);
}

app.post('/preview', async (req, res) => {
const { url } = req.body;
const parsed = new URL(url);

if (parsed.protocol !== 'https:') {
return res.status(400).send('Only https is allowed');
}
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
return res.status(400).send('Host not allowed');
}

const resolvedIp = await resolveDns(parsed.hostname);
if (isPrivateOrLinkLocal(resolvedIp)) {
return res.status(400).send('Destination not allowed');
}

// fetch with redirects disabled, then re-validate the Location
// header against the same checks before following it manually
const response = await fetch(url, { redirect: 'manual' });
res.send(extractPreview(await response.text()));
});

A few things matter here beyond the code:

Allowlist, don't blocklist. Trying to enumerate every bad destination (every private range, every alias for localhost, every encoding trick) is a losing game. Decide what the feature is actually allowed to fetch and reject everything else.

Disable schemes you don't need. A URL fetcher that accepts file:// can read local files, and one that accepts gopher:// can be used to craft raw requests to other protocols entirely. If the feature only ever needs https://, don't parse anything else.

Block private and link-local ranges at the network layer too, not just in application code. A firewall rule denying outbound traffic to 169.254.169.254 and RFC 1918 ranges from the relevant service is a backstop for when the application-level check has a gap.

Use IMDSv2 on AWS. IMDSv2 requires a session token fetched via a PUT request with a custom header before any metadata GET will succeed, which basic SSRF (a single forged GET request) can't produce. It's not a substitute for input validation, but it closes off the single most common exploitation path and should be enforced account-wide, not opt-in per instance.

Validate after redirects, not just the initial URL. An allowlisted, public URL can respond with a 302 to http://169.254.169.254/, and a naive check that only inspects the URL the user submitted will miss it entirely. Disable automatic redirect-following in the HTTP client, inspect the Location header against the same host and IP checks, and only then follow it manually, one hop at a time.

The call we'd make #

Treat any server-side "fetch this URL" feature as untrusted input handling from day one, not as a bug to patch after someone finds it. Allowlist hosts and schemes, resolve and check the actual IP before connecting, re-validate every redirect, and put a network-layer block on the metadata address and private ranges as a second line of defense. Enforce IMDSv2 everywhere on AWS regardless of whether any given service looks vulnerable today. It's a small amount of upfront design work against a class of bug that, per the Capital One case, can end with a full cloud account compromise from a single overlooked endpoint. Combine it with the broader controls in application security best practices and, for anything running in AWS specifically, AWS security best practices.