Server-Side Request Forgery (SSRF) Code Review Guide
Table of Contents
Introduction
Server-Side Request Forgery (SSRF) is a critical security vulnerability that occurs when an attacker can make a server-side application send HTTP requests to an arbitrary destination chosen by the attacker. This allows attackers to abuse the server's trust relationships and access internal services that are not directly accessible from the internet.
SSRF attacks are particularly dangerous in cloud environments where servers often have access to metadata services (like AWS EC2 metadata at 169.254.169.254) that can expose sensitive credentials. A successful SSRF attack can lead to unauthorized access to internal services, data exfiltration, remote code execution, and complete cloud infrastructure compromise.
Server-Side Request Forgery Attack Flow
url=http://169.254.169.254fetch(url)Metadata APIAWS credentialsCloud credentials exposedInternal network scanningFirewall bypassCritical Impact
SSRF vulnerabilities have been responsible for some of the largest data breaches in history, including the 2019 Capital One breach that exposed over 100 million customer records. Cloud metadata services are particularly attractive targets.
Real-World Scenario
Imagine you're reviewing a web application that allows users to fetch content from external URLs, such as a URL preview feature for a social media platform. The developer implemented a simple URL fetcher without proper validation. Here's what the vulnerable code might look like:
Vulnerable Python Code
1# Vulnerable Python code
2from flask import Flask, request
3import requests
4
5@app.route('/fetch-url')
6def fetch_url():
7 url = request.args.get('url')
8 # DANGEROUS: Fetching arbitrary URLs without validation
9 response = requests.get(url)
10 return response.textAn attacker could exploit this by sending: /fetch-url?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
This would make the server fetch AWS credentials from the metadata service and return them to the attacker.
What makes the above code snippet vulnerable to SSRF?
Understanding SSRF
SSRF exploits occur when applications make HTTP requests based on user-supplied data without proper validation. The vulnerability leverages the server's network position, which typically has access to internal resources that external users cannot reach directly.
Common SSRF Attack Targets
| Target | Description | Impact |
|---|---|---|
| Cloud Metadata Services | AWS: 169.254.169.254, GCP: metadata.google.internal | Credential theft, account takeover |
| Internal APIs | localhost, 127.0.0.1, internal hostnames | Unauthorized API access |
| Internal Databases | Redis, Elasticsearch, MongoDB on internal network | Data theft, manipulation |
| Admin Interfaces | Internal admin panels, management interfaces | Privilege escalation |
| File Protocol | file:///etc/passwd | Local file disclosure |
| Other Cloud Services | Internal S3, internal Kubernetes API | Infrastructure compromise |
Types of SSRF
Basic SSRF: Response is returned to attacker
Blind SSRF: No response returned, but requests still made (confirmed via timing or external interaction)
Semi-blind SSRF: Partial response information like status codes or error messages
Finding SSRF Sinks
SSRF sinks are functions that make HTTP requests or load resources from URLs. During code review, identifying these sinks is crucial because they represent points where SSRF can occur if user input reaches them without proper validation.
Common SSRF Sink Patterns
1// Node.js examples
2const axios = require('axios');
3const fetch = require('node-fetch');
4const http = require('http');
5
6// DANGEROUS: Direct URL from user input
7axios.get(userProvidedUrl);
8fetch(userProvidedUrl);
9http.get(userProvidedUrl);
10
11// DANGEROUS: URL construction with user input
12const url = `https://api.example.com/${userPath}`;
13axios.get(url);
14
15// DANGEROUS: Redirect following
16axios.get(url, { maxRedirects: 10 }); // Could redirect to internal
17
18// DANGEROUS: Image/file processing with URL
19sharp(userProvidedImageUrl);
20imagemagick.convert([userProvidedUrl, 'output.png']);Which of these JavaScript patterns is an SSRF sink?
Identifying Input Sources
During code review, identify all sources where URL data can enter your application. These are the entry points where malicious URLs might be injected:
- HTTP Parameters: GET parameters (?url=), POST body data, JSON payloads with URL fields, path parameters (/proxy/{url})
- HTTP Headers: Referer header, X-Forwarded-Host, custom headers with URLs, Host header manipulation
- Webhook URLs: User-configured webhook endpoints, callback URLs, notification URLs
- File Imports: XML/SVG files with external entities, YAML with URL references, JSON with URL fields
- Database Content: URLs stored from previous user input, configuration URLs in database, cached URL values
- Integration Configs: OAuth redirect URLs, API endpoint configurations, CDN origin URLs
Example: Multiple URL Input Sources
1from flask import Flask, request
2import requests
3import xml.etree.ElementTree as ET
4
5def vulnerable_url_sources():
6 # GET parameter - direct user input
7 preview_url = request.args.get('url')
8
9 # POST JSON body - user controlled
10 webhook_url = request.json.get('webhook')
11
12 # HTTP header - can be manipulated
13 referer = request.headers.get('Referer')
14
15 # XML file upload - XXE can contain URLs
16 xml_content = request.files['config'].read()
17 root = ET.fromstring(xml_content)
18 api_url = root.find('endpoint').text
19
20 # Database lookup - could contain malicious URL
21 user_config = User.query.get(user_id).avatar_url
22
23 # ALL these inputs could trigger SSRF
24 # Each needs proper URL validationTracing Data Flow
Understanding how URLs flow through your application is essential for identifying SSRF vulnerabilities. Track URL data from entry points through transformations to HTTP request functions.
Data Flow Analysis Example
1@RestController
2public class UrlPreviewController {
3
4 // 1. SOURCE: URL enters via request parameter
5 @GetMapping("/preview")
6 public ResponseEntity<?> previewUrl(@RequestParam("url") String url) {
7
8 // 2. VALIDATION ATTEMPT: Basic scheme check
9 if (!url.startsWith("http://") && !url.startsWith("https://")) {
10 return ResponseEntity.badRequest().body("Invalid URL");
11 }
12
13 // 3. PROCESSING: URL is transformed
14 String normalizedUrl = normalizeUrl(url); // Still vulnerable!
15
16 // 4. SINK: HTTP request is made
17 return fetchUrlContent(normalizedUrl);
18 }
19
20 private String normalizeUrl(String url) {
21 // Weak normalization - doesn't prevent SSRF
22 return url.trim().toLowerCase();
23 }
24
25 private ResponseEntity<?> fetchUrlContent(String url) {
26 // DANGEROUS: Making request without proper validation
27 RestTemplate restTemplate = new RestTemplate();
28 String content = restTemplate.getForObject(url, String.class);
29 return ResponseEntity.ok(content);
30 }
31}Complex Data Flows
URLs may flow through multiple transformations, encodings, and redirects before reaching a sink. Watch for URL encoding/decoding, redirect chains, URL shorteners, and protocol handlers that could bypass simple validation.
Prevention Techniques
Preventing SSRF requires multiple defensive layers. The most effective approach combines URL allowlisting, network segmentation, and avoiding user-controlled URLs entirely when possible.
Defense Strategies
| Technique | Effectiveness | Implementation |
|---|---|---|
| URL Allowlisting | Highest | Only allow requests to known safe domains |
| IP Address Validation | High | Block private/internal IP ranges |
| DNS Resolution Check | High | Resolve DNS before validation to prevent rebinding |
| Disable Redirects | Medium | Prevent redirect-based bypasses |
| Network Segmentation | High | Isolate application from sensitive services |
| Metadata Service Protection | Critical | Use IMDSv2 or equivalent protections |
Secure Implementation Example
1import ipaddress
2import socket
3from urllib.parse import urlparse
4import requests
5
6# Allowlist of permitted domains
7ALLOWED_DOMAINS = {'api.example.com', 'cdn.example.com'}
8
9# Blocked IP ranges (private, loopback, link-local, metadata)
10BLOCKED_RANGES = [
11 ipaddress.ip_network('10.0.0.0/8'),
12 ipaddress.ip_network('172.16.0.0/12'),
13 ipaddress.ip_network('192.168.0.0/16'),
14 ipaddress.ip_network('127.0.0.0/8'),
15 ipaddress.ip_network('169.254.0.0/16'), # Link-local & metadata
16 ipaddress.ip_network('0.0.0.0/8'),
17]
18
19def is_safe_url(url):
20 """Comprehensive URL validation for SSRF prevention"""
21 try:
22 parsed = urlparse(url)
23
24 # 1. Only allow HTTP/HTTPS schemes
25 if parsed.scheme not in ('http', 'https'):
26 return False
27
28 # 2. Check domain against allowlist
29 hostname = parsed.hostname
30 if hostname not in ALLOWED_DOMAINS:
31 return False
32
33 # 3. Resolve DNS and check IP address
34 resolved_ip = socket.gethostbyname(hostname)
35 ip_obj = ipaddress.ip_address(resolved_ip)
36
37 # 4. Block private/internal IP ranges
38 for blocked_range in BLOCKED_RANGES:
39 if ip_obj in blocked_range:
40 return False
41
42 return True
43 except Exception:
44 return False
45
46def safe_fetch(url):
47 """Safely fetch URL with SSRF protections"""
48 if not is_safe_url(url):
49 raise ValueError("URL not allowed")
50
51 # Disable redirects to prevent bypass
52 response = requests.get(url, allow_redirects=False, timeout=10)
53 return response.text