prevent-ssrf
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
SKILL.mdskills/engineering/security/skills/prevent-ssrf/SKILL.md
GitHubPrevent 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 urlBlock 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'), # AWS/GCP/Azure IMDS 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): return False return not any(ip in net for net in BLOCKED_RANGES)Prevent DNS rebinding — resolve DNS once and use the resolved IP for the actual connection:
import requests from requests.adapters import HTTPAdapter class SSRFSafeAdapter(HTTPAdapter): def send(self, request, **kwargs): # Resolve and validate before sending hostname = urlparse(request.url).hostname if not is_safe_ip(hostname): raise SecurityError("Blocked SSRF attempt") return super().send(request, **kwargs) session = requests.Session() session.mount('http://', SSRFSafeAdapter()) session.mount('https://', SSRFSafeAdapter())Disable HTTP redirects, or re-validate after each redirect:
# Option A: disable redirects entirely response = requests.get(url, allow_redirects=False) # Option B: validate each redirect destination def safe_get(url, max_redirects=3): for _ in range(max_redirects): resp = requests.get(url, allow_redirects=False) if resp.is_redirect: url = resp.headers['Location'] validate_url(url) # re-validate the redirect target else: return resp raise SecurityError("Too many redirects")In cloud environments, enforce IMDS v2 — IMDSv2 requires a PUT request to obtain a token before accessing metadata. Even if SSRF reaches the endpoint, the attacker cannot get credentials without the token step.
AWS: Set
HttpTokens: requiredon EC2 instance metadata options. GCP: Blockmetadata.google.internalvia VPC firewall rules. Azure: Enable IMDS v2 (managed identity with required tokens).Use an isolated outbound proxy for user-facing fetch features — route all user-driven HTTP requests through a dedicated proxy with strict allowlist enforcement, separate from your internal service traffic. Prevents SSRF from reaching internal microservices.
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.254will 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 urlis bypassed byhttp://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.