SSRF (Server-Side Request Forgery) attacks trick your server into making requests to internal resources or external services. When your server fetches a URL provided by users, attackers can target internal APIs, cloud metadata, and more.
Key Takeaways
- 1SSRF can access internal services, cloud metadata, and private networks
- 2Validate URLs AFTER DNS resolution to prevent DNS rebinding
- 3Block private IP ranges, localhost, and link-local addresses
- 4Use allowlists when possible instead of blocklists
- 5Disable redirects or validate each redirect destination
"By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly."
Attack Examples
Before diving into prevention, it's important to understand how SSRF attacks work. Any feature where your server fetches a URL provided by users is a potential attack vector: image uploads, webhooks, URL previews, PDF generators, and API integrations. The following code shows a common vulnerable pattern.
// Vulnerable: Server fetches any URL
app.get('/preview', async (req, res) => {
const url = req.query.url;
const response = await fetch(url); // SSRF!
res.send(await response.text());
});
// Attack payloads:
// ?url=http://localhost/admin
// ?url=http://169.254.169.254/latest/meta-data/ (AWS metadata)
// ?url=http://internal-api/sensitive-data
// ?url=http://192.168.1.1/router-configThis vulnerable code fetches any URL without validation. An attacker could request internal services like http://localhost/admin or cloud metadata endpoints. The attack payloads demonstrate how attackers probe for internal infrastructure, cloud credentials, and private network resources.
Understanding what attackers target helps you appreciate the severity of SSRF vulnerabilities. Internal infrastructure that's normally protected by firewalls becomes accessible when your server makes the request on the attacker's behalf.
Common SSRF Targets
SSRF attacks typically target internal infrastructure that's normally protected by firewalls. The table below shows the most common targets and their potential impact. Cloud metadata services are particularly dangerous because they often expose credentials with broad permissions.
| Target | URL Pattern | Impact |
|---|---|---|
| AWS metadata | http://169.254.169.254/ | IAM credentials, instance data |
| GCP metadata | http://metadata.google.internal/ | Service account tokens |
| Docker API | http://localhost:2375/ | Container escape, RCE |
| Redis/Memcached | http://localhost:6379/ | Cache poisoning, data theft |
| Internal APIs | http://internal-api.local/ | Bypass authentication |
AWS metadata at 169.254.169.254 is the most notorious target: a single successful SSRF request can leak IAM credentials that grant access to S3 buckets, databases, and other AWS services. Capital One's 2019 breach, which exposed 100 million customer records, began with an SSRF vulnerability targeting cloud metadata.
Now that you understand what attackers target, let's look at effective prevention strategies.
Prevention
Effective SSRF prevention requires multiple layers of defense. The most important step is validating URLs after DNS resolution, not just checking the hostname. This approach prevents DNS rebinding attacks where the hostname resolves to a safe IP during validation but a private IP during the actual request.
import dns from 'dns/promises';
import { isIP } from 'net';
// Block private IP ranges
function isPrivateIP(ip) {
const parts = ip.split('.').map(Number);
if (parts[0] === 10) return true; // 10.x.x.x
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 172.16-31.x.x
if (parts[0] === 192 && parts[1] === 168) return true; // 192.168.x.x
if (parts[0] === 127) return true; // 127.x.x.x (localhost)
if (parts[0] === 169 && parts[1] === 254) return true; // 169.254.x.x (link-local)
if (parts[0] === 0) return true; // 0.x.x.x
return false;
}
async function isURLSafe(urlString) {
let url;
try {
url = new URL(urlString);
} catch {
return { safe: false, reason: 'Invalid URL' };
}
// Only allow HTTPS
if (url.protocol !== 'https:') {
return { safe: false, reason: 'Only HTTPS allowed' };
}
// Resolve hostname to IP
const hostname = url.hostname;
let addresses;
if (isIP(hostname)) {
addresses = [hostname];
} else {
try {
const result = await dns.resolve4(hostname);
addresses = result;
} catch {
return { safe: false, reason: 'DNS resolution failed' };
}
}
// Check all resolved IPs
for (const ip of addresses) {
if (isPrivateIP(ip)) {
return { safe: false, reason: `Blocked private IP: ${ip}` };
}
}
return { safe: true, url, addresses };
}This validation function performs several critical checks. First, it only allows HTTPS to prevent protocol-based attacks. Then it resolves the hostname to IP addresses and verifies none are in private ranges. The isPrivateIP function blocks the 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x (localhost), and 169.254.x.x (link-local/metadata) ranges.
Even with IP validation, sophisticated attackers can use DNS rebinding to bypass your checks. Understanding this technique is essential for building robust SSRF defenses.
DNS Rebinding
DNS rebinding is an advanced SSRF technique that exploits the time gap between URL validation and the actual HTTP request. Attackers control a domain's DNS and rapidly change the IP address it resolves to. Your validation passes with a public IP, but the request goes to localhost.
# DNS rebinding attack:
1. Attacker controls evil.com
2. First DNS lookup: evil.com → 1.2.3.4 (public IP, passes validation)
3. Server starts request
4. Attacker updates DNS: evil.com → 127.0.0.1 (localhost!)
5. Request goes to localhost, bypassing the check
# Defense: Resolve DNS, then connect to that IP directly
# Or: Re-validate after any redirectsThe defense against DNS rebinding is to resolve DNS yourself and connect directly to the validated IP address, rather than letting the HTTP library resolve the hostname again. Alternatively, use very short DNS TTLs in your resolver and re-validate the IP immediately before each request.
HTTP redirects present another bypass opportunity. Even if you validate the initial URL, a redirect can send your server to an internal resource.
Handling Redirects
HTTP redirects are a common SSRF bypass technique. An attacker's server returns a 302 redirect to http://169.254.169.254/, and your server dutifully follows it. The safest approach is to disable redirects entirely. When that's not possible, you must validate every redirect destination before following it.
// Disable redirects entirely
const response = await fetch(url, { redirect: 'error' });
// Or validate each redirect
async function safeFetch(url, maxRedirects = 3) {
let current = url;
for (let i = 0; i < maxRedirects; i++) {
const check = await isURLSafe(current);
if (!check.safe) throw new Error(check.reason);
const response = await fetch(current, { redirect: 'manual' });
if (!response.headers.has('location')) {
return response;
}
current = new URL(response.headers.get('location'), current).href;
}
throw new Error('Too many redirects');
}The safeFetch function handles redirects securely by validating each redirect URL before following it. It uses redirect: 'manual' to prevent automatic following, then manually processes the Location header. The redirect limit prevents infinite redirect loops that could be used for denial-of-service attacks.