Defending Against SSRF

SecuritySSRFBackendCloud
Share on LinkedIn

A URL preview feature in our staging app fetched whatever link a user pasted and returned the page title. A security researcher submitted http://169.254.169.254/latest/meta-data/iam/security-credentials/ and got back live AWS credentials in the JSON response. The feature was meant to show "Example Domain" as a title tag. Instead it became a pivot point to our entire AWS account.

Server-side request forgery (SSRF) happens when your application makes HTTP requests to URLs controlled or influenced by an attacker. The attacker doesn't attack your server directly — they trick your server into attacking internal resources on their behalf. Metadata endpoints, internal admin panels, Redis instances on localhost, and services behind firewalls that trust internal traffic all become reachable.

How SSRF attacks work

The pattern is consistent across vulnerabilities:

  1. Your app has a feature that fetches a remote URL (webhook delivery, image proxy, RSS import, PDF rendering).
  2. The attacker supplies a URL pointing to an internal resource.
  3. Your server — sitting inside the network with access to internal services — makes the request.
  4. The attacker reads the response (direct SSRF) or infers success from side effects (blind SSRF).
# Vulnerable webhook tester
@app.post("/webhooks/test")
async def test_webhook(url: str):
    response = httpx.get(url)  # Attacker controls url
    return {"status": response.status_code, "body": response.text[:500]}

An attacker submits http://127.0.0.1:6379/ and your server sends Redis commands. Or http://internal-admin.corp:8080/delete-all and your server triggers admin actions that external attackers can't reach directly.

The metadata endpoint threat

In cloud environments, the link-local metadata service is the highest-value SSRF target:

These endpoints return credentials your instance uses to access cloud APIs. SSRF that reaches them gives attackers the same permissions as your application — often far more than they need.

Mitigate at the infrastructure level: require IMDSv2 on AWS (token-based, not trivially reachable via simple GET), restrict metadata access with network policies, and use minimal IAM roles so compromised credentials have limited blast radius.

URL validation that actually works

Blocklisting private IPs is necessary but insufficient. Implement layered validation:

Parse and validate the URL structure:

from urllib.parse import urlparse
import ipaddress
import socket

BLOCKED_NETWORKS = [
    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"),
    ipaddress.ip_network("0.0.0.0/8"),
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
]

def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        return False
    if not parsed.hostname:
        return False
    if parsed.username or parsed.password:
        return False  # block userinfo bypass tricks

    try:
        resolved = socket.getaddrinfo(parsed.hostname, parsed.port or 80)
    except socket.gaierror:
        return False

    for _, _, _, _, sockaddr in resolved:
        ip = ipaddress.ip_address(sockaddr[0])
        if any(ip in net for net in BLOCKED_NETWORKS):
            return False
    return True

Resolve DNS before connecting — validate the resolved IP, not just the hostname. DNS rebinding attacks change the IP between validation and connection, so resolve once and connect to that specific IP.

Block redirects to internal addresses. Configure your HTTP client to not follow redirects, or re-validate every redirect target:

client = httpx.Client(follow_redirects=False)
# Or if redirects are needed:
client = httpx.Client(event_hooks={"response": [validate_redirect_target]})

Prefer allowlists over blocklists. If your feature only needs to fetch from known partner domains, allowlist those domains instead of trying to block every internal address:

ALLOWED_HOSTS = {"api.stripe.com", "hooks.slack.com", "api.github.com"}

def is_allowed(url: str) -> bool:
    host = urlparse(url).hostname
    return host in ALLOWED_HOSTS

Network-level defenses

Application validation can have bugs. Network policies provide a second line:

Architecture patterns that reduce SSRF risk

Proxy through a dedicated fetch service. One hardened service handles all outbound URL fetching with strict validation, logging, and rate limiting. Other services call it via internal RPC instead of making HTTP requests directly.

Use pre-signed URLs for user content. Instead of fetching user-supplied URLs, have users upload content directly to object storage and process it from there.

Webhook signatures instead of URL testing. When users configure webhooks, deliver a test payload and verify they receive it — don't fetch their URL and return the response body.

Common production mistakes

Teams get ssrf prevention defense wrong in predictable ways:

Production implementations of ssrf prevention defense fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.

Resources

Frequently asked questions

What is the most common SSRF target in cloud environments?

The cloud metadata service at 169.254.169.254 (AWS, GCP, Azure). An SSRF vulnerability that lets an attacker control the URL your server fetches can reach this endpoint and retrieve IAM credentials, instance tokens, and service account keys. This is how SSRF escalates from 'fetch a URL' to full cloud account compromise within seconds.

Does blocking private IP ranges stop all SSRF?

It stops the most common cases — accessing internal services on RFC 1918 addresses and the metadata endpoint. It doesn't stop SSRF to external services you didn't intend (attacker-controlled servers for data exfiltration), DNS rebinding attacks that resolve to internal IPs after validation, or attacks via redirect chains. Defense requires allowlists, network policies, and disabling unnecessary outbound access.

How do I test for SSRF in my application?

Identify every endpoint where your server makes HTTP requests based on user-supplied URLs — webhooks, URL previews, PDF generators, import-from-URL features, OAuth callbacks. Submit internal addresses (127.0.0.1, 169.254.169.254, 10.0.0.1) and observe whether your server connects. Use Burp Collaborator or an external canary server to detect blind SSRF where responses aren't returned to the attacker.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →