Parameter

Server-side request forgery (SSRF)

Server-side request forgery (SSRF) is a vulnerability where an attacker makes a server send HTTP requests to a destination of the attacker's choosing, reaching internal services, cloud metadata endpoints, and other hosts the attacker cannot address directly.

OWASP
A10:2021 Server-Side Request Forgery (SSRF)
Last reviewed

How does SSRF work?

Server-side request forgery happens when an application takes a URL or hostname from user input and makes a server-side request to it. The attacker cannot reach the internal network from outside, but the server can, so pointing that request inward turns the server into a proxy for the attacker.

The vulnerable feature almost always looks like product work: a link-preview box, an avatar-by-URL uploader, a webhook target, a PDF or document importer, an XML parser that resolves external entities. Each takes a URL and has the server fetch it.

Here is a preview feature doing what it was built to do, then doing what it was not:

POST /api/link-preview HTTP/1.1
Host: acme.example
Content-Type: application/json
Cookie: session=user_4821

{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
HTTP/1.1 200 OK
Content-Type: application/json

{"title": "app-ec2-role", "body": "app-ec2-role"}

The server fetched a link-local address it should never touch on a user's behalf. The next request down that path (/latest/meta-data/iam/security-credentials/app-ec2-role) returns temporary cloud credentials. The full chain from a URL field to credential theft is walked through in SSRF to cloud takeover; this page covers the vulnerability class, its variants, and the fix.

An attacker outside the network posts a URL to a link-preview endpoint. The application server, inside the trust boundary, fetches that URL and reaches the metadata service at 169.254.169.254, an internal admin panel and an unauthenticated Redis instance, none of which the attacker can address directly.
The attacker never addresses the internal hosts. The server does it for them.

What does SSRF look like in practice?

The impact depends on what the server can reach and what it hands back.

  • Metadata endpoints. The instance metadata service at 169.254.169.254 returns IAM credentials on hosts still allowing the unauthenticated version. This is the highest-value target in a cloud environment.
  • Internal services. Admin panels, databases, message queues, and internal APIs that trust anything originating from inside the network. SSRF reaches them through the vulnerable server.
  • Blind SSRF. The response never returns to the attacker. Exploitation relies on side channels: response timing, status-code and error-message differences that reveal whether an internal port is open, or an out-of-band callback (a DNS or HTTP hit on a hostname the attacker controls) that proves the server connected.
  • Non-HTTP schemes. file:// reads local files, gopher:// and dict:// smuggle arbitrary bytes to services like Redis or SMTP that speak line-based protocols.

The 2019 Capital One breach is the widely cited real-world case. As publicly reported, an SSRF flaw in a misconfigured web application firewall on an EC2 instance was used to query the metadata service, retrieve the instance role's temporary credentials, and read data from S3 buckets affecting roughly 106 million applicants. Treat that as the publicly reported account, summarized in Krebs on Security.

Filter bypasses

Most SSRF defenses fail because they treat the problem as string matching on the URL. The common bypasses:

BypassExampleWhy the filter misses it
Alternate IP encodings2130706433, 0x7f000001, 0177.0.0.1All decode to 127.0.0.1, none match the literal string
Redirect followingAttacker host returns 302 to 169.254.169.254The filter validates the first URL, the client follows the redirect
DNS rebindingHostname resolves public, then privateFilter check and client connect resolve at different moments
Decimal and IPv6 forms::ffff:169.254.169.254, enclosed alternatesNot on a string blocklist

How do you test for SSRF?

The goal is to prove the server made a request you controlled, not just that a field accepts a URL.

  1. Find the sinks. List every input that becomes a server-side request: URL fields, webhook and callback settings, image or file importers, PDF and HTML renderers, XML and SVG parsers, and any parameter named url, uri, dest, redirect, feed, or webhook.
  2. Point it inward. Try http://127.0.0.1, http://localhost, http://169.254.169.254/latest/meta-data/, and RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). Watch response body, status code, and timing for differences between a reachable and an unreachable target.
  3. Set an out-of-band canary. For blind cases, point the field at a hostname you control (a Burp Collaborator or equivalent) and watch for the DNS or HTTP callback. A DNS-only hit still proves the server resolved your name.
  4. Escalate the reachable ones. Once an internal request lands, walk the metadata tree for credentials, enumerate internal ports, and try non-HTTP schemes if the client supports them.
  5. Defeat the filter. If a blocklist rejects obvious payloads, try the encodings and the redirect and rebinding techniques above, and confirm which one the specific client falls to.

How do you fix SSRF?

SSRF is a trust decision, not a parsing problem. Blocklists lose because there are too many ways to spell an internal address. The pattern that holds is allowlist plus resolve-and-pin: decide the exact destinations the server may reach, resolve the hostname yourself, verify the resolved address, then connect to that address so the client cannot re-resolve to something else.

const dns = require("dns").promises;
const net = require("net");
const http = require("http");

const ALLOWED_HOSTS = new Set(["images.acme-cdn.example"]);

async function safeFetch(rawUrl) {
  const url = new URL(rawUrl);
  if (url.protocol !== "https:") throw new Error("scheme not allowed");
  if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error("host not allowed");

  const { address } = await dns.lookup(url.hostname);
  if (isPrivate(address)) throw new Error("resolved to internal address");

  // Pin the connection to the address we validated so the client
  // cannot re-resolve the name (defeats DNS rebinding).
  return new Promise((resolve, reject) => {
    const req = http.request(
      { host: address, servername: url.hostname, path: url.pathname, protocol: url.protocol },
      resolve,
    );
    req.on("error", reject);
    req.end();
  });
}

function isPrivate(ip) {
  if (net.isIPv4(ip)) {
    const [a, b] = ip.split(".").map(Number);
    return (
      a === 127 || a === 10 || a === 0 ||
      (a === 172 && b >= 16 && b <= 31) ||
      (a === 192 && b === 168) ||
      (a === 169 && b === 254)
    );
  }
  return true; // reject IPv6 unless you explicitly need it, then check it too
}

What does not work on its own:

  • A WAF. It inspects the inbound request, not the outbound one the server makes, and the encodings above slip past signature rules.
  • A blocklist of bad IPs or hostnames. New encodings and redirects defeat it.
  • Validating the URL then fetching by hostname. DNS rebinding changes the answer between the check and the connection. You have to resolve once and pin.

Fix it at the platform layer too, so an app bug is not automatic credential theft: enforce the token-based instance metadata service so a stray GET cannot lift credentials, and block egress to 169.254.169.254 from workloads that never need it. Never echo the raw upstream response back to the caller.

SSRF vs open redirect

An open redirect makes a victim's browser navigate to an attacker-chosen URL; the request comes from the client. SSRF makes the server send the request. The two combine: an open redirect on an allowlisted host is a common way to satisfy an SSRF filter and then bounce the server to an internal target.

Written by Parameter · Last reviewed