SSRF Attacks in Modern APIs: How a Single Request Can Expose Your Entire Infrastructure
A technical deep-dive into Server-Side Request Forgery (SSRF) vulnerabilities in REST and GraphQL APIs, including exploitation techniques, real-world case studies, and defensive coding patterns.
SSRF Attacks in Modern APIs: How a Single Request Can Expose Your Entire Infrastructure
In February 2024, a misconfigured webhook endpoint at a major fintech company allowed attackers to forge internal requests, pivoting from a public API to their AWS metadata service. Within hours, the attacker extracted IAM credentials, accessed S3 buckets containing 2TB of customer data, and established persistent access to the Kubernetes control plane. The total cost of the breach: $4.2 million in damages, regulatory fines, and remediation.
The vulnerability? Server-Side Request Forgery (SSRF)—a deceptively simple flaw that continues to rank among OWASP's most dangerous API vulnerabilities.
SSRF occurs when an API endpoint accepts a user-supplied URL and makes an HTTP request to it from the server side without proper validation. What starts as a "fetch metadata from this URL" feature becomes a tunnel directly into your internal infrastructure.
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 Capital One Breach (2019)
A former AWS employee exploited an SSRF vulnerability in Capital One's web application firewall. By crafting requests to the AWS metadata service, the attacker obtained IAM role credentials with broad S3 access. Over 100 million customer records were exfiltrated. The vulnerability existed because the WAF could be coerced into forwarding requests to arbitrary internal endpoints—a misconfiguration that cost the company $190 million in settlement costs.
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.
Catch Leaked Keys Before Committing
Over 10 million secrets were leaked on GitHub last year. Run GitScan locally to identify hardcoded keys, .env files, and dangerous patterns before they reach your remote repository.
Install GitScan CLI →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
Use Burp Collaborator or interactsh to detect blind SSRF through DNS/HTTP callbacks.
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.