Back to Blog
OpsecForge Security TeamAPI SecuritySources reviewed 2026-07-24

SSRF Attacks in Modern APIs: How a Single Request Can Expose Your Entire Infrastructure

A defensive guide to Server-Side Request Forgery (SSRF) risks in APIs, including URL validation, network controls, cloud metadata protection, and testing.

Primary source: authoritative reference

THREAT BRIEFING

Server-Side Request Forgery (SSRF) occurs when server-side functionality can be induced to request an unintended resource. Features such as webhooks, URL previews, importers, and document renderers need both application and network controls when user input influences outbound requests.

The OWASP SSRF Prevention Cheat Sheet separates cases where destinations can be allowlisted from cases that must reach arbitrary external hosts. That distinction should drive the design.

The Mechanics of SSRF Exploitation

At its core, SSRF exploits trust boundaries. Your API server typically has network access that external users don't—internal microservices, cloud metadata endpoints, database admin interfaces, and Kubernetes APIs. When user input drives outbound requests, that network topology becomes attackable.

Consider this vulnerable Express.js endpoint:

app.post('/fetch-metadata', async (req, res) => {
  const { url } = req.body;
  // DANGEROUS: No validation on the URL
  const response = await fetch(url);
  res.json({ success: true, data: await response.json() });
});

An attacker sends "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role". On AWS EC2, 169.254.169.254 is the Instance Metadata Service (IMDS). The server dutifully fetches its own IAM credentials. Game over.

The code snippets below are defensive examples for systems you own or are authorized to test.

Basic SSRF Attacker provides a URL to internal services like localhost, 169.254.169.254, or internal DNS names. Direct access to cloud metadata, admin panels, or unauthenticated internal APIs.
Blind SSRF Server makes the request but doesn't return the response. Detected through side channels: timing differences, DNS callbacks, or out-of-band interactions via Burp Collaborator or Interactsh.
Protocol Smuggling Using alternative schemes like file://, gopher://, or ftp:// to access local files, Redis commands, or internal services that speak different protocols.
DNS Rebinding Bypassing URL validation by controlling a domain that resolves to different IPs over time—first to a safe external IP, then to internal addresses like 127.0.0.1.

Common Vulnerable Patterns

SSRF typically hides in features that seem harmless: webhooks, PDF generators, thumbnail fetchers, URL previews, and analytics integrations. Anytime your server fetches a user-supplied resource, you're in the danger zone.

The PDF Generator Trap:

@app.route('/export-pdf', methods=['POST'])
def export_pdf():
    url = request.json.get('url')
    
    # Using headless Chrome to render PDF
    subprocess.run([
        'chrome', '--headless', 
        '--print-to-pdf=output.pdf',
        url  # User-controlled URL
    ])
    
    return send_file('output.pdf')

An attacker sends file:///etc/passwd as the URL. Chrome happily renders the local file as a PDF. Now scale that up: file:///proc/self/environ to dump environment variables, file:///var/run/secrets/kubernetes.io/serviceaccount/token to steal Kubernetes service account tokens in containerized environments.

GraphQL Introspection Abuse:

query {
  fetchRemoteContent(url: "http://localhost:8080/admin/users") {
    body
    statusCode
  }
}

GraphQL resolvers that proxy to external URLs often lack SSRF protections, allowing attackers to enumerate internal services through the schema's introspection capabilities.

Defensive Implementation

Effective SSRF defense requires layered controls: input validation, network segmentation, and least-privilege architecture.

Layer 1: URL Parsing and Validation

import ipaddress
import re
from urllib.parse import urlparse

class SSRFValidator:
    BLOCKED_SCHEMES = {'file', 'gopher', 'ftp', 'dict', 'ldap', 'ldaps'}
    BLOCKED_HOSTS = {
        'localhost', '127.0.0.1', '0.0.0.0',
        '169.254.169.254',  # Cloud metadata
        '[::1]', '[::]'
    }
    
    @staticmethod
    def is_internal_ip(hostname):
        """Check if resolved IP is in private ranges."""
        try:
            ip = ipaddress.ip_address(hostname)
            return ip.is_private or ip.is_loopback or ip.is_link_local
        except ValueError:
            # Not an IP, resolve it
            import socket
            try:
                resolved = socket.getaddrinfo(hostname, None)
                for family, _, _, _, sockaddr in resolved:
                    ip = ipaddress.ip_address(sockaddr[0])
                    if ip.is_private or ip.is_loopback or ip.is_link_local:
                        return True
                return False
            except socket.gaierror:
                return True  # Fail closed
    
    @classmethod
    def validate_url(cls, url):
        parsed = urlparse(url)
        
        # Block dangerous schemes
        if parsed.scheme not in {'http', 'https'}:
            raise ValueError(f"Scheme '{parsed.scheme}' not allowed")
        
        # Block internal hostnames
        hostname = parsed.hostname.lower() if parsed.hostname else ''
        if hostname in cls.BLOCKED_HOSTS:
            raise ValueError(f"Hostname '{hostname}' is blocked")
        
        # Block IP-based internal addresses
        if cls.is_internal_ip(hostname):
            raise ValueError(f"IP address '{hostname}' is in private range")
        
        # Block attempts to bypass with URL encoding
        decoded = re.sub(r'%([0-9A-Fa-f]{2})', 
                        lambda m: chr(int(m.group(1), 16)), 
                        url)
        if decoded != url:
            cls.validate_url(decoded)  # Recursive validation
        
        return True

# Usage
@app.route('/webhook', methods=['POST'])
def webhook():
    url = request.json.get('url')
    
    try:
        SSRFValidator.validate_url(url)
        response = requests.get(url, timeout=5)
        return {'status': 'success'}
    except ValueError as e:
        return {'error': str(e)}, 400

Layer 2: Network Segmentation

Even with validation, assume SSRF will happen. Architect your infrastructure to limit the blast radius:

# Kubernetes NetworkPolicy restricting egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-server-policy
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: external-services
    ports:
    - protocol: TCP
      port: 443

Layer 3: Cloud Metadata Protection

AWS, GCP, and Azure all provide mechanisms to mitigate IMDS abuse:

# AWS: Require IMDSv2 with session tokens
aws ec2 modify-instance-metadata-options \
    --instance-id i-1234567890abcdef0 \
    --http-tokens required \
    --http-endpoint enabled

# GCP: Disable legacy metadata endpoints
gcloud compute instances add-metadata instance-name \
    --metadata=disable-legacy-endpoints=TRUE

IMDSv2 requires a PUT request to obtain a session token before accessing metadata, making SSRF exploitation significantly harder since attackers can't easily forge the token retrieval request.

Detection and Testing

Automate SSRF detection in your CI pipeline:

def test_webhook_endpoint_ssrf_protection():
    blocked_urls = [
        'http://localhost:8080/admin',
        'http://169.254.169.254/latest/meta-data/',
        'file:///etc/passwd',
        'http://0x7f000001/',
    ]
    
    for url in blocked_urls:
        response = client.post('/webhook', json={'url': url})
        assert response.status_code == 400

Run outbound-callback tests only in an authorized environment and with infrastructure you control. Production tests should be bounded, observable, and reviewed before execution.

Key Takeaways

SSRF exploits the fundamental trust between services—your API server trusts internal network calls, and attackers exploit that trust through carefully crafted requests.

Defense Checklist:

  • [ ] Parse and validate URLs against allowlists, not blocklists
  • [ ] Resolve hostnames to IPs and validate against private ranges
  • [ ] Implement network policies restricting egress from API servers
  • [ ] Enable IMDSv2 on cloud instances to protect metadata endpoints
  • [ ] Include SSRF payloads in automated security tests

SSRF isn't going away. As long as APIs fetch remote resources, attackers will turn those features into tunnels into your infrastructure. Build your defenses accordingly.

Share this: