Server-Side Request Forgery (SSRF)
On this page
Tricking the Server into Fetching for You#
Server-Side Request Forgery (SSRF) happens when an application takes a URL from a user and fetches it on the server. The problem is positional: the server sits inside the trusted network, so a request the attacker could never make from the outside (to an internal database, an admin-only port, or the cloud metadata service) becomes possible with the server acting as an unwitting proxy. It entered the OWASP Top 10 as its own entry (A10) in 2021, voted in by practitioners because of how often it turns a minor feature into a full cloud compromise; the 2025 edition folds it into A01 Broken Access Control.
Where SSRF Lives#
Any "let the server reach a URL" feature is a candidate:
- Image proxies and thumbnail generators (
?url=https://...) - Webhook configuration that "tests" the endpoint by calling it
- PDF or screenshot renderers that fetch arbitrary pages
- Import features that pull data from a user-supplied link
- Link preview / URL unfurling
- Any HTTP client call whose destination is influenced by user input
A vulnerable fetch is as simple as this:
import requests
# VULNERABLE: the server fetches whatever URL the user supplies
def fetch_preview(url: str) -> bytes:
return requests.get(url, timeout=5).content
The attacker substitutes an internal target for the expected external one. Instead of https://example.com/photo.jpg, they send http://169.254.169.254/latest/meta-data/ (the cloud metadata endpoint, below) or http://127.0.0.1:6379/ to reach a Redis instance bound to localhost that was never meant to face anyone. Because the request originates from the server, host firewalls that block the outside world do not block it.
The Metadata Service: SSRF's Crown Jewel#
The highest-value SSRF target in cloud environments is the instance metadata service, reachable at the link-local address 169.254.169.254. It exists to give a VM information about itself, including, on many configurations, temporary credentials for the instance's cloud role. An SSRF that reaches it can read those credentials and then use them against the wider cloud account, pivoting from "a server that previews images" to "an attacker with the server's cloud permissions." The specific defence is IMDSv2 on AWS, which requires a session token obtained with a PUT request that a simple SSRF GET cannot perform, plus setting the hop limit so the token cannot be proxied. Equivalent hardening exists on other clouds. If you review a URL-fetching feature, ask explicitly whether anything can reach 169.254.0.0/16.
Why Blacklists Fail#
Blocking 127.0.0.1 while allowing everything else feels reasonable and fails immediately, because there are many ways to name an internal address:
- Other loopback forms:
127.0.0.2,0.0.0.0,[::1], and decimal/hex/octal encodings of the IP. - Internal ranges by name: a hostname you control with a DNS record pointing at
10.0.0.5. - The metadata address and other link-local IPs.
- Redirects: the allowlisted URL returns a 302 to an internal address. If your client follows redirects (many do by default), the first request passes the check and the second lands inside.
- DNS rebinding: the hostname resolves to an allowed IP when you validate it, then to an internal IP microseconds later when the request is actually made.
The lesson is that validating the string is not enough; what matters is the address the request actually connects to, after DNS resolution and after every redirect.
Blind SSRF and What It Still Leaks#
Sometimes the response is never returned to the attacker, the server fetches the URL but shows nothing. This is blind SSRF, and it is still dangerous. An attacker confirms it by pointing the fetch at a server they control and watching for the incoming request (an out-of-band interaction), which proves the server made the call. From there they can port-scan the internal network by timing responses (a fast connection-refused versus a slow timeout reveals which internal ports are open), and in cloud environments a blind SSRF that can reach the metadata service may still exfiltrate credentials if any response detail leaks back, for example through an error message or a rendered thumbnail. So "we do not show the fetched content" is not a defence; the request itself is the vulnerability.
The Fix: Allowlist and Validate the Real Destination#
Defend structurally. In order of strength:
- Serve by identifier, not URL. If the feature fetches known resources, map an id to a server-side URL so the user never supplies a URL at all. This eliminates SSRF entirely, the same pattern that fixes traversal and IDOR.
- Allowlist destinations. If the URL must be user-supplied, allow only the exact schemes (
httpsonly) and hostnames the feature needs, rejecting everything else. - Resolve and check the IP, then pin it. Resolve the hostname, reject any address in a private, loopback, or link-local range, and connect to the resolved IP so it cannot change between check and use. Disable redirects, or re-validate every hop.
import ipaddress, socket
def is_safe_public_ip(host: str) -> bool:
# Resolve every address the host maps to and reject internal ones
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 or ip.is_multicast):
return False
return True
print(is_safe_public_ip("localhost")) # False (127.0.0.1 / ::1)
print(is_safe_public_ip("169.254.169.254")) # False (link-local)
ipaddress classifies each resolved address, so localhost, RFC 1918 ranges, and the metadata link-local address are all rejected regardless of how they were written. In production, combine this with disabling redirects (requests.get(url, allow_redirects=False)) and, ideally, connecting to the validated IP directly to close the DNS-rebinding window.
The subtle part is the gap between checking and connecting. If you resolve the hostname to validate it, then pass the original hostname to your HTTP client, the client resolves it again, and DNS rebinding lets the second lookup return an internal address. The only way to fully close this is to validate the resolved IP and then connect to that IP, sending the original hostname only as a Host header or SNI value. Libraries and services exist to do this for you; the important thing is to recognise that validating a hostname and then fetching that hostname is a time-of-check-to-time-of-use race an attacker can win.
Defence in Depth#
- Egress firewall. Restrict the application's outbound traffic to the specific external hosts it legitimately needs (firewalls). Then even a working SSRF cannot reach internal services or the wider internet.
- Network segmentation. Keep sensitive internal services off the network segment the web tier can reach.
- IMDSv2 (or the cloud equivalent) so metadata credentials cannot be read by a simple proxied GET.
- Logging and alerting on URL-fetch activity, especially any attempt to reach private ranges or the metadata address, feeding incident response.
Checklist#
- No feature fetches a raw user-supplied URL without an allowlist and IP validation.
- The HTTP client does not follow redirects to unvalidated destinations.
- Validation resolves the hostname and rejects private, loopback, and link-local addresses, checked against the address actually connected to.
- The instance metadata service requires IMDSv2 (or is otherwise unreachable from the app).
- Outbound egress is restricted at the network layer.
- Tested on labs (PortSwigger SSRF, OWASP Juice Shop) rather than live third-party systems.
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.