Server-Side Request Forgery (SSRF)
Server-Side Request Forgery (SSRF) is a critical vulnerability that allows attackers to make HTTP requests from your server to internal systems, cloud metadata endpoints, and other protected resources. This vulnerability has led to some of the largest data breaches in history, including the 2019 Capital One breach that exposed over 100 million customer records.
What is SSRF?
SSRF occurs when an application fetches a remote resource based on user-supplied input without properly validating the destination. Common features that can be exploited include:
- URL preview/unfurling (like Slack, Discord)
- Webhook integrations
- PDF generators that fetch external resources
- Image/file importers from URLs
- API proxies and gateways
How SSRF Works
Basic SSRF Attack
An attacker provides a malicious URL that the server fetches, allowing access to internal resources:
// Vulnerable: User controls the URL
app.get('/fetch', async (req, res) => {
const url = req.query.url;
// Server fetches whatever URL the user provides
const response = await fetch(url);
const data = await response.text();
res.send(data);
});
// Attacker's request:
// GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
// Returns AWS IAM credentials!Cloud Metadata Attacks
Cloud providers expose instance metadata at predictable IP addresses. SSRF can access these to steal credentials:
# AWS Instance Metadata Service
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/user-data/
# Google Cloud
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
# Azure
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/tokenInternal Port Scanning
Attackers can use SSRF to discover internal services:
# Attacker script to scan internal network
import requests
for port in [22, 80, 443, 3306, 5432, 6379, 27017]:
url = f"http://target.com/fetch?url=http://192.168.1.1:{port}/"
try:
r = requests.get(url, timeout=3)
if "Connection refused" not in r.text:
print(f"[+] Port {port} is open")
except:
passBypassing Filters
Simple blocklists are easily bypassed. Attackers use various techniques:
# IP address obfuscation
http://127.0.0.1 → http://2130706433 (decimal)
http://127.0.0.1 → http://0x7f000001 (hex)
http://127.0.0.1 → http://0177.0.0.1 (octal)
http://127.0.0.1 → http://127.1 (shortened)
http://127.0.0.1 → http://[::1] (IPv6)
# DNS rebinding
http://your-evil-domain.com → initially resolves to allowed IP
→ later resolves to 127.0.0.1
# URL parsing confusion
http://expected.com@evil.com
http://evil.com#expected.com
http://evil.com?url=expected.comReal-World Breaches
Capital One (2019) - An SSRF vulnerability in a WAF configuration allowed an attacker to access AWS metadata and steal IAM credentials. Result: 100+ million customer records exposed, $80 million fine.
Microsoft Exchange (2021) - The ProxyLogon vulnerability chain included an SSRF that allowed attackers to authenticate as the Exchange server itself.
Prevention Strategies
Use an Allow List
Never rely on blocklists. Always use strict allow lists:
const allowedDomains = ['api.github.com', 'api.stripe.com'];
function isAllowedUrl(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTPS
if (url.protocol !== 'https:') {
return false;
}
// Strict domain matching
if (!allowedDomains.includes(url.hostname)) {
return false;
}
return true;
} catch {
return false;
}
}
app.get('/fetch', async (req, res) => {
const url = req.query.url;
if (!isAllowedUrl(url)) {
return res.status(400).json({ error: 'URL not allowed' });
}
// Safe to fetch
const response = await fetch(url);
res.json(await response.json());
});Network-Level Controls
Defense in depth with network segmentation:
# AWS Security Group - Block metadata access
resource "aws_security_group_rule" "block_metadata" {
type = "egress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["169.254.169.254/32"]
security_group_id = aws_security_group.app.id
# Use DENY rule in NACL instead
}
# Better: Use IMDSv2 which requires session tokens
resource "aws_instance" "app" {
metadata_options {
http_tokens = "required" # Enforce IMDSv2
http_put_response_hop_limit = 1
}
}Validate Resolved IPs
Validate the IP address after DNS resolution to prevent DNS rebinding:
import dns from 'dns';
import { isIP } from 'net';
const BLOCKED_RANGES = [
/^127\./, // Localhost
/^10\./, // Private
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private
/^192\.168\./, // Private
/^169\.254\./, // Link-local (metadata)
/^0\./, // Invalid
];
async function isSafeToFetch(hostname) {
return new Promise((resolve, reject) => {
dns.resolve4(hostname, (err, addresses) => {
if (err) return reject(err);
for (const ip of addresses) {
for (const range of BLOCKED_RANGES) {
if (range.test(ip)) {
return resolve(false);
}
}
}
resolve(true);
});
});
}Security Checklist
- Use strict allow lists for outbound requests (never blocklists)
- Validate resolved IP addresses, not just hostnames
- Use IMDSv2 for AWS instances (requires session tokens)
- Segment networks - separate application from internal services
- Block outbound traffic to cloud metadata IPs at firewall level
- Disable unnecessary URL schemes (file://, gopher://, dict://)
- Implement request timeouts to prevent slow attacks
- Log and monitor all outbound requests from application servers
Practice Challenges
View allSSRF Basic
URL fetcher that's too trusting.
SSRF Cloud Metadata
SSRF to AWS metadata endpoint. Credentials exposed.
SSRF Bypass Filters
SSRF with weak URL validation. Many bypasses.
SSRF PDF Generator
PDF generation from URL. Internal network scan.
SSRF Webhook
Webhook URL configuration. SSRF to internal services.