SSRF (Server-Side Request Forgery) happens when an application fetches a resource from a user-controlled URL without properly validating the destination. The attacker does not attack the server directly — they make the server attack on their behalf. Because the request originates from inside the infrastructure, it often crosses firewalls, reaches internal services that should not be accessible, and carries the trust of the internal network.
In cloud environments, SSRF is especially dangerous: it is the classic gateway to stealing temporary credentials via the metadata endpoint.
Where SSRF is born
Any feature that accepts a URL and fetches it on the backend is a candidate:
- “Import your profile photo from a URL”
- Webhooks and integrations (“send events to this URL”)
- PDF/screenshot generators that render a page
- URL validators, link previews, image proxies
- XML parsers with external entities (XXE turning into SSRF)
# Vulnerable endpoint: fetches whatever URL the user sends, unrestricted
@app.post("/import")
def import_url():
url = request.json["url"]
resp = requests.get(url, timeout=5) # <- SSRF
return resp.content
The most valuable target: cloud metadata
Cloud providers expose a metadata endpoint on a link-local IP accessible only from inside the instance. In many environments it hands out temporary credentials for the role attached to the machine:
http://169.254.169.254/latest/meta-data/iam/security-credentials/
If the server makes the request the attacker dictates, they can request that address and receive back AccessKeyId, SecretAccessKey and Token — credentials valid for talking to the cloud API as if they were the application. That is the escalation that turns a “fetch this URL” into a full account compromise.
Specific mitigation: enforce IMDSv2 (token-based flow with
PUT+ header and a hop TTL of 1). It breaks most naive SSRFs, which can only perform a simple GET. But treat it as a layer, not a single solution.
Other common internal targets: unauthenticated databases and caches (http://127.0.0.1:6379 for Redis), internal admin panels, orchestration APIs (Kubernetes, Consul) and good old localhost.
Blind SSRF
The response does not always come back to the attacker. In blind SSRF, the application makes the request but does not return the body. It is still exploitable:
- Side-channel exfiltration: differences in response time or status code reveal whether an internal port is open (internal network scanning).
- Out-of-band interactions: pointing at an attacker-controlled domain (their DNS/HTTP server) and observing the call arrive confirms the SSRF and can leak data via subdomain.
Why naive filters fail — the bypasses
The instinctive reaction is “block localhost and 169.254.169.254”. That is almost never enough. The classic bypasses:
Alternative IP representations:
http://2130706433/ # 127.0.0.1 in decimal
http://0x7f000001/ # 127.0.0.1 in hexadecimal
http://0177.0.0.1/ # octal
http://127.1/ # short form
http://[::1]/ # IPv6 loopback
http://[::ffff:169.254.169.254]/ # IPv4 mapped into IPv6
URL and parsing tricks:
http://169.254.169.254.nip.io/ # DNS that resolves to the internal IP
http://attacker.com@169.254.169.254/ # confuses validators that look only at the textual "host"
http://metadata/ # unicode/encoding characters
Redirect: the attacker points at http://attacker-site.com/r, which answers 302 Location: http://169.254.169.254/.... If the application follows redirects and only validated the initial URL, the filter is bypassed.
DNS rebinding: the attacker’s domain resolves to a public IP on the first lookup (passing validation) and to 169.254.169.254 on the second lookup (at the moment of the real fetch). The window between validating and fetching is the vulnerability. This is why validating the URL string is not enough: what matters is the destination IP at the instant of connection.
The correct defense architecture
SSRF is not solved with a regex. The defense is layered, from code to network.
1. Allowlist, never blocklist
Explicitly define the permitted destinations. If the feature only needs to talk to api.partner.com, only allow that host. Everything else is denied by default.
ALLOWED_HOSTS = {"api.partner.com", "cdn.partner.com"}
def safe_fetch(url):
parts = urlparse(url)
if parts.scheme not in ("https",): # https only
raise ValueError("scheme not allowed")
if parts.hostname not in ALLOWED_HOSTS: # host allowlist
raise ValueError("host not allowed")
# Resolve and validate the IP BEFORE connecting (see step 2)
...
2. Resolve DNS and validate the IP — and connect to that IP
To defeat DNS rebinding and redirects, resolve the name, reject private/reserved IPs and force the connection to use exactly the validated IP (no second resolution):
import ipaddress, socket
BLOCKED_NETS = [
ipaddress.ip_network(n) for n in [
"127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10",
]
]
def ip_allowed(host):
for fam, _, _, _, 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
if any(ip in net for net in BLOCKED_NETS):
return False
return True
Validate all returned IPs (a host can have several A/AAAA records) and revalidate after each redirect — or, more simply, disable redirect following in the HTTP client.
3. Network segmentation (the layer that saves you)
Treat the code as fallible and isolate by network: the workload that performs external fetches should have no route to the metadata endpoint, to the database subnet, or to internal panels. Egress filtering and restrictive security groups turn a successful SSRF into a dead end.
4. Cloud-specific hardening
- IMDSv2 mandatory with
hop limit = 1. - Least-privilege roles — if credentials leak, the blast radius is contained.
- Block, by network route, the application’s access to
169.254.169.254when it doesn’t need metadata.
How we test SSRF
- Catalog every input that becomes a server-side request (URLs, webhooks, parsers, importers).
- Point at a controlled out-of-band listener and confirm the call arrives (proof of SSRF, including blind).
- Test the bypass battery: decimal/hex/octal IP, IPv6,
@,nip.io, 302 redirect, DNS rebinding. - Try to reach
169.254.169.254,127.0.0.1on internal ports, and orchestration services. - Measure real impact: reading metadata, internal scanning, access to unauthenticated services.
Mitigation checklist
- Allowlist of host and scheme (
https) — deny by default. - Resolve DNS, reject private/reserved IPs and connect to the validated IP.
- Do not follow redirects (or revalidate the destination on every hop).
- Network segmentation / egress filtering isolating the workload from metadata and the internal network.
- IMDSv2 mandatory, least-privilege roles.
- Disable dangerous schemes (
file://,gopher://,dict://) in the HTTP client. - Short timeouts and a generic response (don’t return a body/error that serves as an oracle).
SSRF is the vulnerability that best illustrates why “validating the input” and “validating the destination” are different problems. A robust defense never trusts the URL string — it controls which IP, on which network, the server is allowed to connect to.