JustAppSec

SSRF and Request Forgery

When your server makes requests on behalf of an attacker.

0:00

Server-Side Request Forgery (SSRF) occurs when an attacker can make your server send HTTP requests to a destination of their choosing. This turns your server into a proxy — reaching internal services, cloud metadata endpoints, and private networks that the attacker cannot access directly.

Why SSRF matters

Your server likely has access to things the public internet does not:

  • Cloud metadata serviceshttp://169.254.169.254/ on AWS, GCP, and Azure returns IAM credentials, instance identity, and configuration
  • Internal APIs — microservices running on private networks without authentication (because "they're internal")
  • Databases and caches — Redis, Elasticsearch, and other services bound to internal IPs
  • Admin panels — monitoring tools, dashboards, and management interfaces on internal ports

If an attacker can make your server request these URLs, they gain access to all of it.

How SSRF happens

Any feature where the server fetches a URL provided by the user is a potential SSRF vector:

  • URL preview / link unfurling — Slack-style preview of pasted links
  • Webhook delivery — the user specifies a callback URL
  • Image/file import — "import from URL" features
  • PDF generation — server-side HTML-to-PDF renderers that fetch external resources
  • API proxying — endpoints that forward requests to user-specified URLs

Basic example

# Flask — SSRF if url is user-controlled
@app.route("/fetch")
def fetch():
    url = request.args.get("url")
    response = requests.get(url)
    return response.text

An attacker requests:

GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

Your server fetches the AWS metadata endpoint and returns IAM credentials to the attacker.

SSRF variants

Basic SSRF

The server fetches the attacker's URL and returns the response to the attacker. The attacker can read internal resources directly.

Blind SSRF

The server fetches the URL but does not return the response to the attacker. The attacker cannot read the content, but they can:

  • Probe for open ports (based on response times or error messages)
  • Trigger actions on internal services (some services respond to GET requests with side effects)
  • Exfiltrate data via DNS (the fetched URL contains sensitive data in the hostname, which gets logged in DNS queries)

SSRF via redirect

The server validates the initial URL (checks it is not internal), but the target server returns an HTTP redirect to an internal address. If the HTTP client follows redirects, the validation is bypassed.

User provides: https://attacker.com/redirect
attacker.com responds: 302 Location: http://169.254.169.254/...
Server follows redirect to metadata endpoint

SSRF via DNS rebinding

The attacker controls a domain that initially resolves to a public IP (passing validation) but then changes its DNS record to an internal IP before the server makes the actual request.

Defences

1. Do not fetch user-provided URLs (if possible)

The most effective defence. If the feature can work without server-side URL fetching, redesign it. For example:

  • Instead of server-side link preview, use client-side JavaScript (the browser fetches from the user's context, not the server's)
  • Instead of "import from URL", accept file uploads
  • For webhooks, use an event queue where you control the destination

2. Allowlist destinations

If you must fetch URLs, restrict to known-safe destinations:

ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}

parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
    raise ValueError("URL not allowed")

3. Block internal addresses

If an allowlist is not feasible, block requests to private and reserved IP ranges:

import ipaddress

def is_internal(hostname):
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(hostname))
        return ip.is_private or ip.is_loopback or ip.is_link_local
    except (socket.gaierror, ValueError):
        return True  # Fail closed — if we can't resolve it, block it

parsed = urlparse(url)
if is_internal(parsed.hostname):
    raise ValueError("Internal URLs are not allowed")

Important: Resolve the hostname to an IP address and check the IP. Do not just check the hostname string — 169.254.169.254 has no hostname, and internal.company.local would bypass a string-only check.

4. Disable redirects

Configure your HTTP client to not follow redirects automatically:

response = requests.get(url, allow_redirects=False)

If the response is a redirect, validate the new location before following it.

5. Block the metadata endpoint

On cloud platforms, use IMDSv2 (AWS), which requires a special header for metadata access. Configure your instances to require IMDSv2:

# AWS — require IMDSv2 (PUT-based token required)
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890 \
  --http-tokens required

This prevents simple GET-based SSRF from reaching the metadata service.

6. Network-level controls

Use firewall rules or VPC security groups to restrict which destinations your application server can reach. If the application should only talk to specific internal services, block everything else at the network level.

7. Use a dedicated HTTP client with restrictions

Create a hardened HTTP client for user-initiated requests:

  • DNS resolution validation (check IP is not internal before connecting)
  • Timeout limits (prevent slow-loris style abuse)
  • Response size limits
  • No redirect following (or limited, validated redirects)
  • Restricted protocols (HTTP/HTTPS only — block file://, gopher://, ftp://)

Protocol-specific tricks

Attackers will try non-HTTP protocols to interact with internal services:

  • file:///etc/passwd — read local files
  • gopher://internal-redis:6379/... — send raw TCP commands to Redis
  • dict://internal-host:11211/ — interact with Memcached

Only allow http:// and https:// schemes.

Summary

SSRF exploits the trust your server has on its network. Any feature that fetches user-provided URLs is a potential vector. Prefer not fetching user URLs at all. If you must, allowlist destinations, block internal IPs (after DNS resolution), disable redirects, restrict protocols, enforce IMDSv2 on cloud instances, and apply network-level controls. Layer these defences — no single one is sufficient against a determined attacker.


This training content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly. Send corrections to [email protected].