Server-Side Request Forgery turns your own server into the attacker's proxy. They give you a URL, your server fetches it, and suddenly they're making requests from inside your network, to places they could never reach from outside. That's what makes SSRF so valuable to them and so dangerous to you. The destinations they're after:
- Cloud metadata.
http://169.254.169.254/hands back IAM credentials on AWS. - Internal APIs. The microservices that skip auth because they assume nothing external can call them.
- Databases and caches. A Redis or Elasticsearch sitting on an internal IP with no authentication.
How it gets in
Any feature where your server fetches a URL the user supplied is a candidate: link previews, webhooks, "import from URL," PDF generation, API proxying.
@app.route("/fetch")
def fetch():
url = request.args.get("url")
return requests.get(url).text # SSRF
The attacker just points it inward:
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
The variants worth knowing
Basic SSRF. The server returns the response, and the attacker reads internal resources directly.
Blind SSRF. No response comes back, but they can still probe ports, trigger actions, and exfiltrate data over DNS.
SSRF via redirect. You validate the initial URL, it passes, and then it redirects to an internal address you never checked.
DNS rebinding. The hostname resolves to a public IP when you validate it, then flips to an internal IP by the time you connect.
Defending against it
Don't fetch user URLs if you can avoid it. Generate previews client-side, accept an upload instead of a URL, or queue webhook deliveries through an event system rather than fetching live.
When you genuinely must fetch, layer these:
Allowlist the destinations you actually need:
ALLOWED_HOSTS = {"api.example.com"}
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("URL not allowed")
Resolve the hostname and block internal IPs, so a name that points inward gets rejected:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
if ip.is_private or ip.is_loopback:
raise ValueError("Internal URL")
Disable redirects, so the server can't be bounced inward after the check: requests.get(url, allow_redirects=False).
Require IMDSv2, which forces a token header before the AWS metadata service responds, defeating the simplest credential grab.
Restrict the protocols to http:// and https://. Block file://, gopher://, and friends.
Add network controls. Firewall rules on outbound traffic mean that even a successful SSRF has nowhere to go.
The thread running through all of these: SSRF is an abuse of the trust your network places in your own server. Stop fetching user-controlled URLs where you can, and where you can't, treat every URL as if it's aimed at 169.254.169.254 until you've proven otherwise.
