Your application fetches a URL a user supplied — a webhook target, an image to thumbnail, a PDF to render. That single feature can hand an attacker a foothold inside your network, and most teams never notice until an incident report lands on their desk. So what is SSRF, exactly, and why does one innocent-looking URL fetch cause so much damage?
What is SSRF? The one-paragraph answer
Server-Side Request Forgery (SSRF, CWE-918) is a vulnerability where an attacker coerces a server into sending HTTP or other network requests to a destination of the attacker's choosing. Because the request originates from inside your trust boundary, it can reach things the attacker never could directly: cloud metadata endpoints, internal admin panels, databases bound to localhost, and services protected only by network position. SSRF is ranked in the OWASP Top 10 (A10:2021) and was the pivot point in the 2019 Capital One breach, where an attacker abused SSRF to read AWS instance-metadata credentials and exfiltrate data on more than 100 million people.
How SSRF actually works
Any time your backend takes a URL, hostname, IP, or file path influenced by user input and turns it into an outbound request, you have a potential SSRF sink. The classic escalation targets are:
- Cloud metadata services. On AWS the link-local address
169.254.169.254serves instance credentials to whatever process asks. IMDSv1 required no authentication, which is exactly what made Capital One exploitable. IMDSv2 added a token-based, hop-limited handshake specifically to blunt SSRF. - Internal services.
http://localhost:6379,http://10.0.0.5:8500, or a Kubernetes control plane that assumes anything on the pod network is trusted. - Cloud and CI internals. GCP metadata, Azure IMDS, and CI runner tokens all live at predictable internal addresses.
Attackers defeat naive defenses with encoding and redirect tricks: decimal or octal IP encoding (http://2130706433/ is 127.0.0.1), DNS names that resolve to internal IPs, an HTTP endpoint that returns a 302 redirect to 169.254.169.254, or alternate schemes like file://, gopher://, and dict:// to reach non-HTTP services. This class was also the mechanism behind CVE-2021-26855 (ProxyLogon), where an unauthenticated SSRF into Microsoft Exchange kicked off one of the most widely exploited attack chains of the decade.
Vulnerable vs. fixed
A webhook validator that fetches whatever the user submits:
# VULNERABLE — fetches any URL, follows redirects, no destination checks
import requests
def validate_webhook(url: str):
resp = requests.get(url, timeout=5) # attacker sends http://169.254.169.254/...
return resp.status_code == 200
The fix combines scheme allow-listing, DNS resolution before the request, an explicit block of private/link-local ranges, and disabling redirects so a 302 can't smuggle the request to an internal target:
# FIXED — validate scheme, resolve host, reject private ranges, no redirects
import ipaddress, socket, requests
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"https"}
def is_public_ip(host: str) -> bool:
for family, _, _, _, sockaddr in socket.getaddrinfo(host, None):
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
return True
def validate_webhook(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError("scheme not allowed")
if not is_public_ip(parsed.hostname):
raise ValueError("destination not allowed")
resp = requests.get(url, timeout=5, allow_redirects=False)
return resp.status_code == 200
Note the ordering: you resolve and validate the host, then make the request. Validating the string alone loses to DNS rebinding, where the name passes your check and then re-resolves to an internal IP on the actual connection. The robust pattern is to resolve once, confirm the address is public, and connect to that pinned address.
Prevention checklist
- Deny by default. Maintain an allow-list of destination hosts or domains rather than trying to block every dangerous one.
- Block internal ranges at the application layer: loopback, RFC 1918, link-local (
169.254.0.0/16), and cloud metadata IPs — after DNS resolution. - Restrict schemes to
https(andhttponly if you truly need it). Rejectfile://,gopher://,dict://,ftp://. - Disable automatic redirect following, or re-validate the destination on every hop.
- Enforce IMDSv2 on AWS and set a metadata hop limit of 1 so containers can't reach it. Apply the equivalent hardening on GCP and Azure.
- Segment the network so a compromised fetch service cannot route to databases, admin planes, or the control plane.
- Send outbound fetches through an egress proxy that enforces destination policy centrally, instead of trusting every service to get it right.
- Log and alert on outbound requests to internal or metadata addresses — that traffic is almost always malicious.
How Safeguard helps
SSRF is a runtime behavior, so it needs runtime evidence. Safeguard's DAST engine actively probes URL-handling endpoints with encoded IPs, redirect chains, and metadata payloads to confirm whether a request-forging sink is genuinely exploitable — not just theoretically present. At the code layer, Griffin AI code review traces user-controlled input into outbound request calls (requests.get, fetch, HttpClient, curl) and flags fetches that skip destination validation, follow redirects, or accept arbitrary schemes. When a vulnerable dependency introduces the sink, Safeguard's software composition analysis maps it against your dependency graph, and Safeguard's auto-fix can open a pull request that adds destination validation or pins a patched version. Running scans locally before you push is a one-liner with the Safeguard CLI, so SSRF gets caught in development rather than in a breach post-mortem.
Curious how our runtime and code analysis stack up against SAST-first tools? See the Safeguard vs Checkmarx comparison or browse the full product comparison hub.
Create a free account at app.safeguard.sh/register and point it at a repository to see SSRF sinks surfaced in minutes, or read the integration guides at docs.safeguard.sh.