flawopen.com/Server-Side Request Forgery/PHP

Server-Side Request Forgery in PHP

High Severity CWE-918
ELI5

You ask a web service to preview a link. You pass 'http://localhost:3306'. The PHP server connects directly to its internal MySQL database server and sends back the connection banner.

Key terms on this page
FILTER_FLAG_NO_PRIV_RANGE
PHP filter flag that rejects RFC 1918 private IPv4 addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
CURLOPT_FOLLOWLOCATION
cURL option that follows 3xx HTTP redirects. If enabled without IP verification on redirect, attackers bypass filters.

What's happening

In PHP, file_get_contents($url) or naive curl_exec($ch) fetches user-supplied URLs without restricting IP addresses.

Real-world impact

SSRF vulnerabilities in PHP web applications have repeatedly been used to exploit internal Redis and FastCGI servers (via gopher:// or http://) to achieve remote code execution.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
<?php
// VULNERABLE: file_get_contents on user-controlled URL
$url = $_GET['url'];

// Attacker input: http://169.254.169.254/latest/meta-data/
$content = file_get_contents($url);

echo $content;
?>
FIXED
<?php
// HARDENED: Parse URL, resolve DNS, filter private IPs, and disable redirects
$url = $_GET['url'] ?? '';

$parsed = parse_url($url);
if (!$parsed || !in_array(strtolower($parsed['scheme'] ?? ''), ['http', 'https'], true)) {
    http_response_code(400);
    exit('Invalid URL scheme');
}

$host = $parsed['host'] ?? '';
$ips = gethostbynamel($host);

if (!$ips) {
    http_response_code(400);
    exit('Unable to resolve host');
}

// Check every resolved IP address
foreach ($ips as $ip) {
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
        http_response_code(403);
        exit('Access to private/internal address denied');
    }
}

// Fetch using cURL with redirects disabled
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false, // Prevent redirect bypasses
    CURLOPT_TIMEOUT        => 3,
    CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
]);

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

echo $response;
?>

Why the fix works

The code validates schemes with parse_url and resolves all IPs with gethostbynamel. filter_var(..., FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) rejects internal addresses, while CURLOPT_FOLLOWLOCATION => false prevents 302 bypasses.

Gotchas

file_get_contents follows redirects

file_get_contents() automatically follows redirects by default without checking target IPs. Use cURL instead.

Common misconceptions

"Disallowing 'localhost' is enough"

Attackers use 127.0.0.1, 0.0.0.0, [::1], 2130706433, or custom domains that resolve to 127.0.0.1. String matching fails.

How to check if you're affected

psalm --taint-analysis phpstan analyse

Prevention checklist

FAQ

Can attackers use gopher:// in PHP?

Yes, if cURL protocols are not restricted. Attackers use gopher:// to send raw commands to Redis or Memcached. CURLOPT_PROTOCOLS eliminates this.

References