SSRF Attacks in Modern APIs: How a Single Request Can Expose Your Entire Infrastructure
A defensive guide to Server-Side Request Forgery (SSRF) risks in APIs, including URL validation, network controls, cloud metadata protection, and testing.
Primary source: authoritative reference
Server-Side Request Forgery (SSRF) occurs when server-side functionality can be induced to request an unintended resource. Features such as webhooks, URL previews, importers, and document renderers need both application and network controls when user input influences outbound requests.
The OWASP SSRF Prevention Cheat Sheet separates cases where destinations can be allowlisted from cases that must reach arbitrary external hosts. That distinction should drive the design.
The Mechanics of SSRF Exploitation
At its core, SSRF exploits trust boundaries. Your API server typically has network access that external users don't—internal microservices, cloud metadata endpoints, database admin interfaces, and Kubernetes APIs. When user input drives outbound requests, that network topology becomes attackable.
Consider this vulnerable Express.js endpoint:
app.post('/fetch-metadata', async (req, res) => {
const { url } = req.body;
// DANGEROUS: No validation on the URL
const response = await fetch(url);
res.json({ success: true, data: await response.json() });
});
An attacker sends "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role". On AWS EC2, 169.254.169.254 is the Instance Metadata Service (IMDS). The server dutifully fetches its own IAM credentials. Game over.
The code snippets below are defensive examples for systems you own or are authorized to test.
Common Vulnerable Patterns
SSRF typically hides in features that seem harmless: webhooks, PDF generators, thumbnail fetchers, URL previews, and analytics integrations. Anytime your server fetches a user-supplied resource, you're in the danger zone.
The PDF Generator Trap:
@app.route('/export-pdf', methods=['POST'])
def export_pdf():
url = request.json.get('url')
# Using headless Chrome to render PDF
subprocess.run([
'chrome', '--headless',
'--print-to-pdf=output.pdf',
url # User-controlled URL
])
return send_file('output.pdf')
An attacker sends file:///etc/passwd as the URL. Chrome happily renders the local file as a PDF. Now scale that up: file:///proc/self/environ to dump environment variables, file:///var/run/secrets/kubernetes.io/serviceaccount/token to steal Kubernetes service account tokens in containerized environments.
GraphQL Introspection Abuse:
query {
fetchRemoteContent(url: "http://localhost:8080/admin/users") {
body
statusCode
}
}
GraphQL resolvers that proxy to external URLs often lack SSRF protections, allowing attackers to enumerate internal services through the schema's introspection capabilities.
Defensive Implementation
Effective SSRF defense requires layered controls: input validation, network segmentation, and least-privilege architecture.
Layer 1: URL Parsing and Validation
import ipaddress
import re
from urllib.parse import urlparse
class SSRFValidator:
BLOCKED_SCHEMES = {'file', 'gopher', 'ftp', 'dict', 'ldap', 'ldaps'}
BLOCKED_HOSTS = {
'localhost', '127.0.0.1', '0.0.0.0',
'169.254.169.254', # Cloud metadata
'[::1]', '[::]'
}
@staticmethod
def is_internal_ip(hostname):
"""Check if resolved IP is in private ranges."""
try:
ip = ipaddress.ip_address(hostname)
return ip.is_private or ip.is_loopback or ip.is_link_local
except ValueError:
# Not an IP, resolve it
import socket
try:
resolved = socket.getaddrinfo(hostname, None)
for family, _, _, _, sockaddr in resolved:
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local:
return True
return False
except socket.gaierror:
return True # Fail closed
@classmethod
def validate_url(cls, url):
parsed = urlparse(url)
# Block dangerous schemes
if parsed.scheme not in {'http', 'https'}:
raise ValueError(f"Scheme '{parsed.scheme}' not allowed")
# Block internal hostnames
hostname = parsed.hostname.lower() if parsed.hostname else ''
if hostname in cls.BLOCKED_HOSTS:
raise ValueError(f"Hostname '{hostname}' is blocked")
# Block IP-based internal addresses
if cls.is_internal_ip(hostname):
raise ValueError(f"IP address '{hostname}' is in private range")
# Block attempts to bypass with URL encoding
decoded = re.sub(r'%([0-9A-Fa-f]{2})',
lambda m: chr(int(m.group(1), 16)),
url)
if decoded != url:
cls.validate_url(decoded) # Recursive validation
return True
# Usage
@app.route('/webhook', methods=['POST'])
def webhook():
url = request.json.get('url')
try:
SSRFValidator.validate_url(url)
response = requests.get(url, timeout=5)
return {'status': 'success'}
except ValueError as e:
return {'error': str(e)}, 400
Layer 2: Network Segmentation
Even with validation, assume SSRF will happen. Architect your infrastructure to limit the blast radius:
# Kubernetes NetworkPolicy restricting egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-server-policy
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: external-services
ports:
- protocol: TCP
port: 443
Layer 3: Cloud Metadata Protection
AWS, GCP, and Azure all provide mechanisms to mitigate IMDS abuse:
# AWS: Require IMDSv2 with session tokens
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabled
# GCP: Disable legacy metadata endpoints
gcloud compute instances add-metadata instance-name \
--metadata=disable-legacy-endpoints=TRUE
IMDSv2 requires a PUT request to obtain a session token before accessing metadata, making SSRF exploitation significantly harder since attackers can't easily forge the token retrieval request.
Detection and Testing
Automate SSRF detection in your CI pipeline:
def test_webhook_endpoint_ssrf_protection():
blocked_urls = [
'http://localhost:8080/admin',
'http://169.254.169.254/latest/meta-data/',
'file:///etc/passwd',
'http://0x7f000001/',
]
for url in blocked_urls:
response = client.post('/webhook', json={'url': url})
assert response.status_code == 400
Run outbound-callback tests only in an authorized environment and with infrastructure you control. Production tests should be bounded, observable, and reviewed before execution.
Key Takeaways
SSRF exploits the fundamental trust between services—your API server trusts internal network calls, and attackers exploit that trust through carefully crafted requests.
Defense Checklist:
- [ ] Parse and validate URLs against allowlists, not blocklists
- [ ] Resolve hostnames to IPs and validate against private ranges
- [ ] Implement network policies restricting egress from API servers
- [ ] Enable IMDSv2 on cloud instances to protect metadata endpoints
- [ ] Include SSRF payloads in automated security tests
SSRF isn't going away. As long as APIs fetch remote resources, attackers will turn those features into tunnels into your infrastructure. Build your defenses accordingly.