SSRF: attackers make your server send HTTP requests to destinations they choose. Your server becomes a proxy to:
- Cloud metadata -
http://169.254.169.254/returns IAM creds - Internal APIs - microservices without auth
- Databases/caches - Redis, Elasticsearch on internal IPs
How it happens
Any feature where server fetches user-provided URL: 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
Attacker: GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Variants
Basic SSRF: Server returns response. Attacker reads internal resources.
Blind SSRF: No response, but can probe ports, trigger actions, exfiltrate via DNS.
SSRF via redirect: Validate initial URL, but target redirects to internal address.
DNS rebinding: Domain resolves to public IP (passes check), then changes to internal IP.
Defences
Don't fetch user URLs. Client-side preview, accept uploads instead of URLs, event queue for webhooks.
Allowlist destinations:
ALLOWED_HOSTS = {"api.example.com"}
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("URL not allowed")
Block internal IPs: Resolve hostname, check the IP:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
if ip.is_private or ip.is_loopback:
raise ValueError("Internal URL")
Disable redirects: requests.get(url, allow_redirects=False)
Require IMDSv2: AWS metadata requires token header.
Network controls: Firewall rules restricting outbound destinations.
Restrict protocols: Only http:// and https://. Block file://, gopher://.
The takeaway
SSRF exploits your server's network trust. Don't fetch user URLs if possible. When you must: allowlist destinations, block internal IPs after DNS resolution, disable redirects, enforce IMDSv2, apply network controls.
