Server-Side Request Forgery (SSRF)
debt(d7/e5/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list semgrep and psalm, which are specialist SAST tools that can catch the basic pattern of curl_exec/file_get_contents with user-supplied input. However, sophisticated bypasses like DNS rebinding, alternate URL schemes (gopher://, file://), and post-resolution IP validation failures are not reliably caught by static analysis alone — runtime testing or manual review is required for those. This places it above d5 (specialist tool) but not quite d9 (silent in production), landing at d7.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says to validate and allowlist target URLs server-side, but common_mistakes reveal that a complete fix requires: blocking private IP ranges after DNS resolution, preventing DNS rebinding (re-checking IP at connection time), blocking dangerous schemes (file://, gopher://, dict://), and replacing raw user-supplied URLs throughout the codebase. This is more than a single-line patch or simple parameter swap — it requires implementing a dedicated URL validation layer that must be applied consistently across all fetch points.
Closest to 'localised tax' (b3). The applies_to scope is web and CLI contexts, and the fix primarily involves adding URL validation logic around fetch operations. While it must be applied consistently wherever user-supplied URLs are used, it does not impose a cross-cutting architectural burden on the entire codebase — it's scoped to components that make outbound HTTP requests. It's slightly above b1 (single utility function) because the validation must be consistently applied across those fetch points.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field explicitly states: 'SSRF is only dangerous if the server can reach the internet' — when in fact it is most dangerous for reaching internal services that are otherwise unreachable from the outside. This is a documented, serious cognitive trap: developers assume blocking public internet access is sufficient protection, but the real attack surface (cloud metadata at 169.254.169.254, internal admin panels, local databases) is invisible until exploited. DNS rebinding bypass also contradicts the intuitive belief that hostname validation is sufficient.
Also Known As
TL;DR
Explanation
SSRF allows attackers to make the server issue requests to arbitrary URLs — including internal services (cloud metadata endpoints, databases, admin interfaces) not exposed to the public internet. In cloud environments, SSRF against 169.254.169.254 can expose IAM credentials. Mitigation: validate and allowlist target URLs, block private IP ranges, reject non-http(s) schemes, and prefer dedicated HTTP client libraries with configurable restrictions.
How It's Exploited
# AWS metadata endpoint — returns instance credentials
POST webhook_url=http://internal-admin.corp/secret
Diagram
flowchart TD
ATK[Attacker] -->|fetch http://internal-api/secret| APP[Vulnerable App<br/>fetch URL param]
APP -->|request from server| INT[Internal API<br/>192.168.1.100]
APP -->|request from server| META[AWS Metadata<br/>169.254.169.254]
INT & META -->|sensitive data| APP --> ATK
subgraph Fix
ALLOW[Allowlist external URLs<br/>Block internal IP ranges<br/>169.254/10.x/172.16/192.168]
end
style FIX fill:#238636,color:#fff
style INT fill:#f85149,color:#fff
style META fill:#f85149,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Fetching user-supplied URLs with curl_exec() or file_get_contents() without validating the resolved IP.
- DNS rebinding bypass: validating the hostname but not re-checking the IP at connection time.
- Not blocking private IP ranges (10.x, 172.16.x, 192.168.x, 127.x, 169.254.x) after DNS resolution.
- Allowing file://, gopher://, or dict:// URL schemes alongside http://
Code Examples
$url = $_POST['webhook_url'];
$resp = file_get_contents($url); // SSRF — attacker controls the URL
function safeRequest(string $url): string {
$parsed = parse_url($url);
// Only allow HTTPS to public internet
if ($parsed['scheme'] !== 'https') throw new \InvalidArgumentException('HTTPS required');
// Block private/internal IP ranges
$ip = gethostbyname($parsed['host']);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
throw new \InvalidArgumentException('Private/reserved IP not allowed');
}
return (new GuzzleHttp\Client())->get($url)->getBody()->getContents();
}