SSRF — Server Fetches Internal or Metadata URLs
Mitigate SSRF by validating URLs against an allowlist, blocking private and link-local ranges, and disabling redirects to internal hosts.
Symptoms
- An endpoint that fetches a user-supplied URL can reach 169.254.169.254
- A scanner reports SSRF, or the app returns internal service responses
- Redirects from an allowed host reach an internal address
Possible causes
- The server fetches any URL the user provides without validation
- Only the initial host is checked, so redirects reach private ranges
- DNS resolution points an allowed name at an internal IP (DNS rebinding)
Diagnosis steps
Probe internal targets
curl -i "https://api.example.com/fetch?url=http://169.254.169.254/latest/meta-data/"Interpretation
A successful response with metadata content confirms SSRF; a block shows mitigation is present.
Inspect fetch sinks and validation
grep -rn "fetch(\|axios\|http.get\|requests.get" src/ | headInterpretation
Each outbound fetch driven by user input needs allowlist and private-range checks.
Root cause
SSRF occurs when the server performs a request to an attacker-controlled destination, typically a URL parameter. This lets the attacker reach internal services, cloud metadata endpoints, or bypass network controls.
Fix
Use a strict allowlist of hostnames and schemes, resolve the host and reject private, loopback, and link-local IP ranges, re-check after each redirect, and prefer a dedicated egress proxy for outbound fetches.
Risk note
Checking only the hostname is insufficient against DNS rebinding; validate the resolved IP and pin the connection.
Verify the fix
Resend internal and metadata probes and confirm they are blocked; verify an allowlisted public URL still fetches successfully.
Symptoms
When you hit “SSRF — Server Fetches Internal or Metadata URLs”, guessing wastes time. The guide below follows evidence: reproduce the symptom, run targeted diagnostics, identify the root cause, apply the fix, and verify it with concrete checks.
Observed symptoms include:
- An endpoint that fetches a user-supplied URL can reach 169.254.169.254
- A scanner reports SSRF, or the app returns internal service responses
- Redirects from an allowed host reach an internal address
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Probe internal targets
curl -i "https://api.example.com/fetch?url=http://169.254.169.254/latest/meta-data/"
A successful response with metadata content confirms SSRF; a block shows mitigation is present.
Step 2: Inspect fetch sinks and validation
grep -rn "fetch(\|axios\|http.get\|requests.get" src/ | head
Each outbound fetch driven by user input needs allowlist and private-range checks.
Likely contributors, in order of frequency:
- The server fetches any URL the user provides without validation
- Only the initial host is checked, so redirects reach private ranges
- DNS resolution points an allowed name at an internal IP (DNS rebinding)
Root Cause
SSRF occurs when the server performs a request to an attacker-controlled destination, typically a URL parameter. This lets the attacker reach internal services, cloud metadata endpoints, or bypass network controls.
The Fix
Use a strict allowlist of hostnames and schemes, resolve the host and reject private, loopback, and link-local IP ranges, re-check after each redirect, and prefer a dedicated egress proxy for outbound fetches.
Watch out: Checking only the hostname is insufficient against DNS rebinding; validate the resolved IP and pin the connection.
import { lookup } from 'node:dns/promises';
import net from 'node:net';
const ALLOWED = new Set(['api.partner.com', 'cdn.example.com']);
function isPrivate(ip) {
return /^(127\.|10\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(ip)
|| ip === '::1' || ip.startsWith('fc') || ip.startsWith('fd');
}
export async function safeFetch(rawUrl) {
const url = new URL(rawUrl);
if (url.protocol !== 'https:' || !ALLOWED.has(url.hostname)) throw new Error('blocked host');
const { address } = await lookup(url.hostname);
if (isPrivate(address) || net.isIP(address) === 0) throw new Error('blocked address');
return fetch(url, { redirect: 'manual' }); // do not auto-follow into private ranges
}
Verify and Prevent
Resend internal and metadata probes and confirm they are blocked; verify an allowlisted public URL still fetches successfully.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.