Most applications that fetch remote resources treat user supplied URLs as harmless strings. The server dutifully makes the request, returns the content, and never considers that the URL might point inward instead of outward. An endpoint that pulls an avatar from a profile URL or fetches a webhook callback becomes a pivot point into internal infrastructure the attacker could never reach directly.
The gap is simple: developers think about what the URL points to in terms of expected use cases, not where it can be made to point. A feature designed to fetch public images can just as easily request the cloud metadata service at 169.254.169.254, internal admin panels at localhost:8080, or services behind the firewall that assume the network perimeter is sufficient authentication.
Why it hides
Server Side Request Forgery is invisible to the user and often invisible in logs. The application works exactly as designed when given legitimate URLs. Code review shows a standard HTTP client library making a request, nothing that looks dangerous on its face. There is no stack trace, no error condition, no failed assertion. The vulnerability only surfaces when someone intentionally supplies a hostile destination, and by then the request has already left the server with all the trust and network access that implies.
The method
- Identify every parameter, header, or body field where the application accepts a URL, URI, or hostname. This includes obvious cases like webhook callbacks and image fetchers, but also less visible ones like XML external entity references, PDF generators that accept HTML with remote stylesheets, and import features that pull data from a user specified endpoint.
- Test whether the application resolves and requests internal addresses. Start with localhost, 127.0.0.1, and the private IP ranges. Check if the server will connect to its own listening ports or other services on the internal network. Observe response times, error messages, and whether content is returned or reflected.
- Probe for cloud metadata endpoints. In AWS, Azure, and GCP environments, the metadata service exposes IAM credentials, instance details, and configuration. Test whether
169.254.169.254or the equivalent platform specific address can be reached. Check if the application returns the response body or leaks it through error messages or side channels. - Attempt to bypass naive filters. If the application blocks certain domains or IP ranges, test for parser differentials using alternate encodings like octal, hex, or decimal IP notation, URL fragments, redirects through attacker controlled domains, DNS rebinding, or international domain names that resolve to blocked addresses.
- Examine whether the application respects redirects. Many SSRF filters only check the initial URL. If the server follows HTTP 3xx responses, an attacker can host a redirect on a permitted domain that forwards the request to an internal target.
- Check for protocol abuse. Some libraries allow schemes beyond HTTP and HTTPS. Test for
file://,gopher://,dict://, or other protocols that might read local files or interact with internal services in unexpected ways. - Review whether the application leaks response content or metadata. Even if the full response is not returned to the user, timing differences, error messages, or boolean success indicators can confirm the existence and state of internal resources.
The deeper nuance
Defensive approach
When I build features that fetch remote resources, I start with an allowlist of permitted schemes, domains, and IP ranges. I validate the URL before and after DNS resolution to catch redirects and rebinding attacks. I disable or limit redirect following, and I ensure the HTTP client cannot access internal IP ranges or the metadata service.
const ALLOWED_HOSTS = ['cdn.example.com', 'api.partner.com'];
const BLOCKED_RANGES = ['127.0.0.0/8', '169.254.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
function isUrlSafe(url) {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
if (!ALLOWED_HOSTS.includes(parsed.hostname)) return false;
const resolved = dns.resolve(parsed.hostname);
if (BLOCKED_RANGES.some(range => ipInRange(resolved, range))) return false;
return true;
}
Why it stays a problem
SSRF persists because the features that enable it are genuinely useful. Webhooks, integrations, and remote content fetching are core to modern applications. The vulnerability is not in the library or the protocol, but in the implicit trust that user input points where it should. Developers add URL validation as an afterthought, if at all, and cloud environments make the stakes higher by placing sensitive metadata a single HTTP request away.
Server Side Request Forgery turns the application into a proxy for the attacker. I test every place a URL is accepted, validate before and after resolution, and assume the network perimeter is not an authorization boundary.