Skip to content

CWE-918: Server-Side Request Forgery (SSRF)

Overview

Server-Side Request Forgery (SSRF) occurs when an application fetches remote resources based on user-supplied URLs without proper validation. Attackers can force the server to make requests to arbitrary destinations, including internal services, cloud metadata endpoints, or other protected resources. SSRF exploits the server's trusted position in the network.

OWASP Classification

A01:2025 - Broken Access Control

Risk

High to Critical: Attackers can access internal services not exposed to the internet, read cloud metadata containing credentials (AWS, Azure, GCP), bypass firewalls and access controls, scan internal networks, exfiltrate sensitive data, or perform denial-of-service attacks. Cloud environments are particularly vulnerable to credential theft via metadata endpoints.

Remediation Strategy

Use URL Allowlists (Primary Defense)

Only allow requests to known, trusted destinations:

  • Maintain an explicit allowlist of permitted domains/IPs
  • Validate the full URL (scheme, host, port, path) against the allowlist
  • Reject any URL not on the allowlist
  • Use exact matches or carefully controlled regex patterns
  • Never use denylists - attackers will bypass them

Block Private IP Ranges and Special Hostnames (Defense in Depth)

Prevent access to internal networks and metadata services:

  • Block private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Block loopback: 127.0.0.0/8, localhost
  • Block link-local: 169.254.0.0/16 (AWS/Azure metadata)
  • Block metadata hostnames: metadata.google.internal, etc.
  • Perform DNS resolution and check resolved IPs
  • Critical: Implement DNS pinning protection (see below)

DNS Pinning Attack Prevention:

DNS pinning is a bypass technique where attackers:

  1. Create a domain that initially resolves to a legitimate IP
  2. Application validates the IP (passes allowlist)
  3. Attacker changes DNS to point to internal IP (127.0.0.1, 192.168.x.x)
  4. Application makes request using cached DNS or re-resolves
  5. Request goes to internal service

Protection against DNS pinning:

# Validate BOTH before DNS resolution AND after

from urllib.parse import urlparse
import socket

def is_safe_url(url):
    parsed = urlparse(url)
    hostname = parsed.hostname

    # Step 1: Validate hostname against allowlist
    if hostname not in ALLOWED_DOMAINS:
        raise SecurityException("Domain not allowed")

    # Step 2: Resolve DNS and validate ALL resolved IPs
    try:
        ip_addresses = socket.getaddrinfo(hostname, None)
        for ip_info in ip_addresses:
            ip = ip_info[4][0]
            if is_private_ip(ip):
                raise SecurityException(f"Domain resolves to private IP: {ip}")
    except socket.gaierror:
        raise SecurityException("Cannot resolve hostname")

    # Step 3: Re-validate just before making request
    # (implement with minimal TTL caching)
    return True

URL Parser Bypass Prevention:

Different languages parse URLs differently, allowing bypasses:

Example bypass techniques:

  • http://127.0.0.1@evil.com (parsed differently by Python vs Java)
  • http://[::ffff:127.0.0.1]/ (IPv6 notation for IPv4)
  • http://0x7f.0x0.0x0.0x1/ (Hex encoding)
  • http://2130706433/ (Decimal IP notation for 127.0.0.1)

Remediation Steps

Core principle: Never allow untrusted input to determine the destination or scope of server-side outbound requests; all outbound request targets must be strictly defined and enforced by the server. Treat outbound requests as privileged: enforce URL allowlists, safe parsing/canonicalization, and network egress controls.

Locate the SSRF vulnerability in your code

  • Analyze how untrusted data influences HTTP requests
  • Identify the source: where URL/destination data enters (user input, external files, databases, API parameters)
  • Trace URL construction: how URLs are built from untrusted data
  • Locate the sink: HTTP request functions (requests.get(), HttpClient, fetch(), curl_exec())

Implement URL allowlists (Primary Defense)

  • Maintain explicit list of permitted domains/IPs: Define ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com']
  • Validate full URL components: Check scheme (http/https), host, port, and path against allowlist
  • Use exact matching: Exact domain matches or carefully controlled regex patterns - avoid wildcards
  • Reject unlisted URLs: Any URL not on allowlist is rejected with security exception
  • Never use denylists: Attackers will find bypasses - allowlists only
  • Why this works: Users can only trigger requests to pre-approved destinations

Block private IP ranges and metadata endpoints (Defense in Depth)

  • Block private IPs: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Block loopback: 127.0.0.0/8, localhost, ::1
  • Block link-local: 169.254.0.0/16 (AWS/Azure metadata at 169.254.169.254)
  • Block cloud metadata hostnames: metadata.google.internal, instance-data (AWS), metadata.azure.com
  • Prevent DNS pinning attacks: Validate hostname, resolve DNS and validate ALL resolved IPs, re-validate before request
  • Reject if any resolved IP is private: Even if hostname is allowed, reject if it resolves to private IP

Implement additional SSRF protections

  • Network segmentation: Run application in isolated network, restrict outbound connections
  • Use HTTP client restrictions: Disable redirects or validate redirect destinations, set timeouts, limit response size
  • Validate URL parsing: Use same parser for validation and request, handle URL encoding consistently
  • Block alternative IP formats: Reject hex (0x7f.0.0.1), decimal (2130706433), IPv6-mapped IPv4 ([::ffff:127.0.0.1])
  • Implement request signing: For webhooks, use HMAC signatures to verify legitimate requests

Monitor and audit SSRF attempts

  • Log all outbound HTTP requests (destination URL, source user/IP, timestamp)
  • Alert on requests to blocked destinations (private IPs, metadata endpoints, rejected domains)
  • Monitor for SSRF attack patterns (rapid requests to different IPs, DNS rebinding attempts)
  • Track allowlist effectiveness (requests blocked vs allowed)
  • Review and update allowlist regularly (add legitimate destinations, remove unused ones)

Test the SSRF protection thoroughly

  • Test with allowed domains (should succeed)
  • Test with private IPs: 127.0.0.1, 192.168.1.1, 10.0.0.1 (should be rejected)
  • Test with metadata endpoints: 169.254.169.254, metadata.google.internal (should be rejected)
  • Test with URL parser bypasses: http://user@host format, IPv6 notation, alternative IP encodings
  • Test with DNS rebinding: domain that changes resolution mid-request
  • Re-scan with security scanner to confirm the issue is resolved

Prevent URL parser bypasses:

  • http://127.0.0.1@evil.com - parser confusion
  • http://[::ffff:127.0.0.1]/ - IPv6 notation for IPv4
  • http://0x7f.0x0.0x0.0x1/ - hex encoding
  • http://2130706433/ - decimal IP (127.0.0.1)
  • http://evil.com#@127.0.0.1/ - fragment abuse

Use proper URL parsing libraries and validate after parsing.

Disable or Validate Redirects

Prevent bypass via HTTP redirects:

  • Disable automatic redirects: Set redirect following to false
  • If redirects needed: Validate each redirect destination against allowlist
  • Limit redirect depth: Maximum 3-5 redirects
  • Re-validate IPs: Check resolved IPs after each redirect

Example (Python):

import requests

# Disable redirects
response = requests.get(url, allow_redirects=False)

# Or validate each redirect
for redirect in response.history:
    if not is_allowed_url(redirect.url):
        raise SecurityException("Redirect to disallowed URL")

Use Network Segmentation and Least Privilege

Limit impact of successful SSRF:

  • Network segmentation: Run application in restricted network segment
  • Egress filtering: Limit outbound connections at firewall
  • Disable URL schemes: Block file://, gopher://, dict://, etc.
  • Least privilege: Minimal IAM permissions for service accounts
  • No cloud credentials: Don't attach IAM roles if not needed

Test with SSRF Payloads

Verify your protections:

Internal network access:

  • http://localhost/admin
  • http://127.0.0.1:8080/
  • http://192.168.1.1/
  • http://10.0.0.1/

Cloud metadata:

  • http://169.254.169.254/latest/meta-data/ (AWS)
  • http://metadata.google.internal/computeMetadata/v1/ (GCP)
  • http://169.254.169.254/metadata/instance?api-version=2021-02-01 (Azure)

URL encoding bypasses:

  • http://127.0.0.1http://0x7f.0.0.1
  • http://[::ffff:127.0.0.1]/
  • http://2130706433/ (decimal)

Ensure all are blocked and legitimate external URLs still work:

  • http://metadata.google.internal/computeMetadata/v1/
  • http://192.168.1.1/

Common Vulnerable Patterns

  • Using user-provided URLs without validation: fetch(userUrl), urllib.request.urlopen(url)
  • Webhook implementations that don't validate destinations
  • Image/file fetchers that accept arbitrary URLs
  • Proxy endpoints that forward requests
  • URL shortener services without destination checks

Unvalidated URL Fetch with Metadata Access (JavaScript)

// Fetching URL from user input without validation
url = request.getParameter("imageUrl")
response = httpClient.get(url)  // SSRF vulnerability
// Attack: url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
// Result: Attacker retrieves AWS credentials from metadata service

// Webhook without allowlist
webhookUrl = request.getParameter("callback")
httpClient.post(webhookUrl, data)  // SSRF vulnerability
// Attack: webhookUrl = "http://internal-admin-panel:8080/deleteAllUsers"
// Result: Attacker accesses internal services

Secure Patterns

Domain Allowlist with Private IP Blocking (Python)

// Validate URL against allowlist before fetching
allowedDomains = ["cdn.example.com", "api.example.com"]
url = request.getParameter("imageUrl")

parsedUrl = parseURL(url)
if parsedUrl.hostname not in allowedDomains:
    throw SecurityException("Domain not allowed")
if isPrivateIP(resolveHostname(parsedUrl.hostname)):
    throw SecurityException("Private IP not allowed")

response = httpClient.get(url)  // SAFE - validated

Why this works:

  • Restricts outbound requests to pre-approved domains, preventing access to internal services
  • Blocks private IP ranges (10.x, 192.168.x, 127.x, 169.254.x) to prevent metadata service access
  • Resolves hostname to IP before validation, preventing DNS rebinding attacks
  • Prevents attackers from accessing cloud metadata services (AWS, Azure, GCP) to steal credentials
  • Protects internal admin panels, databases, and services from external manipulation

Verification Steps

  • All private IP ranges are blocked
  • Metadata endpoints are inaccessible
  • Only allowlisted domains are permitted
  • Redirects are disabled or validated
  • Business functionality still works
  • Test with automated SSRF scanners

Why this works:

  • Comprehensive attack testing: Ensures all SSRF vectors tested, not just obvious ones
  • Private IP protection: Confirms blocking of internal network access
  • Cloud credential defense: Verifies metadata endpoint inaccessibility (AWS, Azure, GCP) prevents credential theft
  • Allowlist enforcement: Confirms only intended domains accessible
  • Redirect bypass prevention: Validates handling of HTTP 30x responses
  • Functionality preservation: Ensures security controls don't break legitimate use cases
  • Automated coverage: Scanner testing catches edge cases and bypass techniques manual testing might miss

Language-Specific Guidance

For detailed, language-specific examples and framework-specific patterns:

  • C# - HttpClient, WebClient with URL filtering
  • Java - HttpClient, RestTemplate, OkHttp with allowlists
  • JavaScript/Node.js - axios, fetch, got, http with SSRF prevention
  • PHP - cURL, file_get_contents with hostname validation
  • Python - requests, urllib, httpx with URL validation

Dynamic Scan Guidance

For guidance on remediating this CWE when detected by dynamic (DAST) scanners:

Additional Resources