📋 What Is It?
Server-Side Request Forgery (SSRF) occurs when an API fetches a remote resource without validating the user-supplied URL. Attackers can trick the server into making requests to internal services, cloud metadata endpoints, or arbitrary external URLs, bypassing firewalls and accessing sensitive data.
API07
OWASP Rank
Internal
Network Access
Cloud
Metadata Exposure
⚠️ Common Exploits
- Cloud Metadata Access: Fetch AWS/Azure/GCP credentials from metadata endpoints
- Internal Network Scanning: Probe internal IPs and ports
- Local File Access: Read files using file:// protocol
- Internal API Access: Call internal services not exposed externally
- Firewall Bypass: Access services protected by firewall
- Port Scanning: Enumerate open ports on internal systems
🔴 Attack Flow
1. Attacker finds URL parameter in API
↓
2. Sends internal URL: http://169.254.169.254/
↓
3. Server fetches AWS metadata endpoint
↓
4. Returns IAM credentials in response
↓
5. BREACH: AWS account compromised!
↓
2. Sends internal URL: http://169.254.169.254/
↓
3. Server fetches AWS metadata endpoint
↓
4. Returns IAM credentials in response
↓
5. BREACH: AWS account compromised!
❌ Vulnerable Code
// Bad: No URL validation!
@app.route('/api/fetch', methods=['POST'])
def fetch_url():
url = request.json.get('url')
# VULNERABLE: Fetches any URL!
response = requests.get(url)
return response.text
// Attack examples:
POST /api/fetch
{
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}
← AWS credentials!
{
"url": "http://localhost:8080/admin"
}
← Internal admin panel!
{
"url": "file:///etc/passwd"
}
← Local file access!
// Bad: Webhook without validation
@app.route('/api/webhook', methods=['POST'])
def register_webhook():
webhook_url = request.json.get('url')
# Stores and calls user-provided URL
save_webhook(current_user.id, webhook_url)
return {'status': 'registered'}
✅ Secure Code
// Good: Strict URL validation
import ipaddress
from urllib.parse import urlparse
ALLOWED_DOMAINS = ['example.com', 'api.partner.com']
BLOCKED_IPS = [
ipaddress.ip_network('127.0.0.0/8'), # localhost
ipaddress.ip_network('169.254.0.0/16'), # link-local (AWS metadata)
ipaddress.ip_network('10.0.0.0/8'), # private
ipaddress.ip_network('172.16.0.0/12'), # private
ipaddress.ip_network('192.168.0.0/16'), # private
]
def is_safe_url(url):
parsed = urlparse(url)
# Only allow HTTP/HTTPS
if parsed.scheme not in ['http', 'https']:
return False
# Whitelist allowed domains
if parsed.hostname not in ALLOWED_DOMAINS:
return False
# Resolve hostname and check IP
try:
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
for blocked_network in BLOCKED_IPS:
if ip in blocked_network:
return False
except:
return False
return True
@app.route('/api/fetch', methods=['POST'])
def fetch_url():
url = request.json.get('url')
# Validate URL before fetching
if not is_safe_url(url):
return {'error': 'Invalid URL'}, 400
try:
response = requests.get(
url,
timeout=5,
allow_redirects=False # Prevent redirect attacks
)
return response.text
except requests.RequestException as e:
return {'error': 'Failed to fetch'}, 500
// Good: Use predefined resources instead of URLs
@app.route('/api/avatar', methods=['POST'])
def set_avatar():
# Instead of accepting URL, use resource ID
avatar_id = request.json.get('avatar_id')
# Fetch from predefined list
avatar_url = PREDEFINED_AVATARS.get(avatar_id)
if not avatar_url:
return {'error': 'Invalid avatar'}, 400
current_user.avatar = avatar_url
return {'status': 'updated'}
✓ Prevention Checklist
- Whitelist allowed domains/protocols
- Block private IP ranges (RFC1918, link-local)
- Disable unused URL schemes (file://, gopher://, etc.)
- Validate and sanitize all user-supplied URLs
- Disable HTTP redirects or validate redirect targets
- Use DNS resolution validation
- Implement network segmentation
- Block cloud metadata IPs (169.254.169.254)
- Use resource identifiers instead of URLs when possible
- Set request timeouts
- Log all outbound requests
- Apply principle of least privilege for network access
🔍 Detection & Tools
Testing Tools:
Burp Suite
OWASP ZAP
SSRFmap
Gopherus
curl
Postman
Prevention Libraries:
SafeCurl
SSRF-Filter
urllib3
requests
How to Test:
- Try localhost: http://127.0.0.1, http://localhost
- Test metadata: http://169.254.169.254/
- Try file:// protocol for local files
- Test internal IPs: 10.0.0.1, 192.168.1.1
- Use URL encoding/obfuscation bypasses
- Test redirect chains
🌍 Real-World Breaches
- Capital One (2019): SSRF to AWS metadata led to 100M+ records breach
- Shopify (2020): SSRF vulnerability allowed internal network access
- Verizon (2017): SSRF exposed internal systems and credentials
- Uber (2016): SSRF in internal tools led to data exposure
- Atlassian (2019): SSRF in Jira allowed AWS metadata access
📌 Quick Tips
- DO NOT fetch user-supplied URLs without validation
- DO NOT trust URL schemes beyond HTTP/HTTPS
- DO NOT allow access to 169.254.169.254
- DO whitelist allowed domains
- DO block private IP ranges
- DO disable redirects or validate targets
- DO use network segmentation
📜 Compliance
Related Standards:
- OWASP ASVS V5.2
- PCI-DSS Requirement 6.5.1
- NIST 800-53 SC-7
- ISO 27001 A.13.1.3
- SOC 2 CC6.6