| name | prevent-ssrf |
| description | Use when building features that fetch URLs or make HTTP requests based on user-supplied input — web scrapers, webhook receivers, URL preview services, import-from-URL features, or any server-side HTTP client. |
| source | OWASP Server-Side Request Forgery Prevention Cheat Sheet (owasp.org/www-project-cheat-sheets); OWASP Top 10 2021 A10; CWE-918 |
| tags | ["security","owasp","ssrf","server-side-request-forgery","web","developer","cloud"] |
Prevent SSRF
Block server-side request forgery by validating and allowlisting URLs before making server-side HTTP requests — preventing attackers from using your server as a proxy to reach internal metadata endpoints, databases, or cloud infrastructure.
Why This Is Best Practice
Adopted by: OWASP added SSRF as its own category in Top 10 2021 (A10) due to increasing severity in cloud environments. AWS, GCP, and Azure now offer IMDS v2 (token-required metadata endpoints) specifically to mitigate SSRF. Google exposed $31,337 in SSRF bug bounties. Capital One's 2019 breach (100M records) was executed entirely via SSRF exploiting AWS EC2 metadata at 169.254.169.254.
Impact: In cloud environments, SSRF can reach the instance metadata service (IMDS) and retrieve IAM credentials, enabling full cloud account takeover without any direct database access. Capital One breach (2019) affected 100M+ records via a WAF SSRF vulnerability. Netflix, GitLab, and Shopify have all paid significant SSRF bug bounties. In containerized environments, SSRF can reach Kubernetes API servers on internal networks.
Why best: Denylist-based approaches (blocking 169.254.169.254, localhost, 127.0.0.1) fail because attackers bypass them with DNS rebinding, IPv6 addresses (::1, ::ffff:127.0.0.1), decimal encoding (2130706433 = 127.0.0.1), or redirects. Allowlist-based validation (permitting only known external domains) is the only reliable defense.
Sources: OWASP SSRF Prevention Cheat Sheet; Capital One breach report (2019); AWS IMDS v2 documentation; CWE-918
Steps
-
Use an allowlist of approved URL patterns — define which domains/URLs your feature should reach:
ALLOWED_DOMAINS = {'api.example.com', 'uploads.example.com', 'cdn.partner.com'}
from urllib.parse import urlparse
import ipaddress
def validate_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValueError("Only HTTP/HTTPS allowed")
hostname = parsed.hostname
if hostname not in ALLOWED_DOMAINS:
raise ValueError(f"Domain not allowed: {hostname}")
return url
-
Block all private and link-local IP ranges after DNS resolution — resolve the hostname and check the resulting IP, not just the domain name:
import socket
import ipaddress
BLOCKED_RANGES = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'),
ipaddress.ip_network('::1/128'),
ipaddress.ip_network('fc00::/7'),
]
def is_safe_ip(hostname):
try:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
except (socket.gaierror, ValueError):
(ip net net BLOCKED_RANGES)
Rules
- DNS rebinding bypasses hostname checks — always resolve and check the IP, not just the hostname.
- Redirects are SSRF vectors — an allowlisted URL that redirects to
http://169.254.169.254 will bypass hostname-only checks.
- IPv6 addresses, decimal IP encoding, and URL-encoded characters are all bypass vectors — use a normalized IP comparison after resolution.
- File URI (
file:///etc/passwd) and other non-HTTP schemes must be blocked explicitly.
Common Mistakes
- Blocking by regex pattern on the URL string —
if '169.254' in url is bypassed by http://0251.0376.0251.0376/ (octal encoding).
- Allowing redirects without re-validation — the initial URL passes the check, then a redirect targets an internal service.
- Only protecting web-facing features — PDF generators, email link unfurling, and webhook delivery systems are common SSRF vectors.
- Denylist without allowlist — blocking known bad IPs creates a whack-a-mole game; allowlist known good domains instead.