Why Internal Networks Are the Most Trusted and Least Verified Part of Modern Infrastructure
Server-side request forgery doesn't compromise a credential. It borrows a network position that was never checked in the first place — because nobody was ever assigned to check it.
Server-side request forgery is a class of vulnerability where an application, in the course of doing something legitimate, can be induced to make an HTTP (or other protocol) request to a destination the attacker chose rather than the destination the application's author had in mind. The application has some feature that fetches a URL on the server's behalf — an image proxy that downloads a thumbnail, a webhook validator that pings a callback, a PDF renderer that loads a page, an "import from URL" button, a link-preview generator. Somewhere in that feature, a URL that originated from user input gets handed to the server's own HTTP client, and the server issues the request as itself. If that URL isn't restricted to the destinations the feature was actually built for, the attacker has just gotten a server — with all of its network position, none of its authentication burden — to make a request they couldn't make directly.
That last clause is the entire mechanism. The attacker doesn't need credentials for whatever they're targeting. They need a server that already has network access to it, and a way to make that server ask on their behalf.
What Makes It Different From "Just Another Injection Bug"
SQL injection and command injection get a request's data trusted when it shouldn't be. SSRF gets a request's origin trusted when it shouldn't be — and the reason that distinction matters is that huge amounts of internal infrastructure authenticate almost nothing beyond origin. A cloud instance metadata endpoint reachable at 169.254.169.254 typically requires no credential at all; it hands back temporary IAM credentials, instance configuration, sometimes user-data scripts, to whatever process asks, because the only thing standing between "asking" and "authorized to ask" is being able to reach that address — which, by design, only the instance itself can do. An internal admin panel, a metrics endpoint, a service mesh sidecar, a database exposed on a private subnet: each of these was very plausibly built with the assumption that anything able to reach it on the network had already cleared some other bar to get there. SSRF is what happens when that assumption turns out to be load-bearing and nobody load-tested it.
A widely cited example of this exact chain — a web application vulnerability used to reach a cloud instance metadata endpoint, which returned credentials subsequently used to access stored data at scale — is the 2019 breach affecting a major U.S. financial institution. The specific mechanics, dates, and CVE identifiers involved should be independently verified against primary sources (court filings, the affected company's disclosures, CISA/NVD records) before being cited as fact in any published version of this essay.
The pattern generalizes past cloud metadata. Anywhere a network boundary is the only thing enforcing "you're not supposed to be able to do this," a server that can be tricked into making requests from inside that boundary inherits everything the boundary was supposed to keep out. Internal Redis and Memcached instances with no auth configured, because "nothing outside the VPC can reach them." Internal Kubernetes API servers reachable from any pod on the cluster network. Internal build servers, internal package registries, internal CI runners — the entire category of infrastructure that gets provisioned with "it's on the private subnet" standing in for "it's authenticated."
The Mechanics Attackers Actually Use
A naive defense — block requests to 127.0.0.1, 169.254.169.254, and RFC 1918 private ranges — is where most SSRF mitigation starts and, if it stops there, where it usually fails. The gap between a blocklist and the actual space of reachable destinations is large, and every entry in the list below is a documented technique, not a hypothetical:
- DNS rebinding. A domain the allowlist check resolves to a public IP at validation time, then re-resolves to an internal IP at request time (attacker controls the DNS TTL and the authoritative nameserver). The check and the fetch are two different network calls; nothing guarantees they see the same address.
- Redirect chains. The application validates the initial URL, gets a 200-clean public destination, and the HTTP client — configured to follow redirects, which is almost always the default — follows a
3xxresponse to an internal address the validator never saw. - Alternate IP encodings. Decimal, octal, and hexadecimal representations of an IP address (
2130706433for127.0.0.1), IPv6 loopback and IPv4-mapped forms, or embedding credentials in the URL to confuse a naive parser — all of these can slip past a regex-based blocklist that only recognizes the canonical dotted-quad form. - Protocol confusion. An allowlist scoped to
http://andhttps://doesn't stop a URL-fetching library that also honorsfile://,gopher://, ordict://schemes, each of which reaches a different class of internal resource than an HTTP request would. - Blind SSRF. Even when the response body is never returned to the attacker, timing differences between "connection refused," "connection timed out," and "got a response" are enough to map which internal hosts and ports exist — reconnaissance without ever seeing the actual data.
None of these are exotic. They're the standard toolkit, which is itself informative: SSRF is not a hard vulnerability to exploit once a URL-fetching feature exists. It's a vulnerability that's hard to fully close, because closing it requires the fetching code to reason correctly about DNS resolution timing, redirect behavior, IP address parsing, and protocol handling all at once — a wider surface than "check the domain looks okay."
Treat the URL-fetching path as a network egress control problem, not an input-validation problem. A denylist checked against the URL string before the request is made will always be racing against DNS rebinding and redirects that resolve differently at request time. The only mitigation that closes the actual gap is enforcing the restriction at the point of the outbound connection itself — a network-layer egress proxy or firewall rule that the fetching code cannot bypass by choosing a different code path, and that is re-checked at connection time, not validation time.
Why "Just Add a Denylist" Keeps Not Being Enough
The response to all of this, inside most engineering organizations, tends to go through the same sequence. First pass: block the obvious loopback and metadata addresses at the application layer, in the code that validates the user-supplied URL. This stops the laziest exploitation attempts and almost nothing else, for the reasons above — DNS rebinding and redirects both happen after the check, not during it. Second pass, after someone finds the gap: move validation later, resolve the DNS name before checking, compare the resolved IP against the blocklist. This closes rebinding-at-validation-time but still races against rebinding-at-connection-time if the resolution and the connection are two separate steps with any gap between them, and it still says nothing about redirects, which resolve and connect on a completely separate code path inside whatever HTTP client library is following them.
The pattern that actually holds is inverting the whole approach: instead of asking "does this destination look internal," ask "is this connection, at the moment the socket opens, going somewhere on an explicit allowlist of destinations this feature is supposed to reach." An image-fetching feature that only ever needs to hit a known set of CDNs and object storage buckets doesn't need denylist logic at all — it needs an egress proxy or firewall rule that simply has no route to anything else, so a rebound DNS answer or a malicious redirect has nowhere to land even if the application code is fooled. This is also the reasoning behind IMDSv2 on AWS, which requires a session token obtained via an explicit PUT request before any metadata can be read via GET — a design specifically intended to break the simplest class of SSRF, where an attacker can only induce the victim server to make a single, attacker-controlled GET request and has no way to add the token-fetching step in front of it. It doesn't eliminate SSRF against the metadata service; it raises the bar from "any URL-fetching bug" to "a URL-fetching bug that also lets the attacker control an HTTP method and a custom header," which is a meaningfully smaller set of vulnerabilities.
SSRF surface is broader than the obvious "fetch this URL" feature. Any library that parses a format capable of referencing external resources can carry the same risk internally — PDF renderers that follow embedded links, XML parsers that resolve external entities (the related and equally well-documented XXE class), image libraries that follow embedded metadata references, webhook and callback-URL validators that ping the destination to confirm it's reachable before saving it. Each of these is, mechanically, the same server-makes-a-request-to-attacker-controlled-destination pattern, wearing a different feature's clothing.
Why This Reads as an Organizational Failure, Not Just a Coding Mistake
Here's the thing that makes SSRF the clearest case in this series, more than any of the other five: the trust decision it exploits was never made by an application developer at all. The application developer wrote a feature that fetches a URL. The decision that made the exploit possible — "anything that can reach this internal service on the network doesn't need to authenticate further, because reaching it already proves something" — was made by whoever designed the network topology, months or years earlier, for reasons that had nothing to do with the fetch feature that would eventually abuse it. The metadata service wasn't built insecure; it was built for a threat model where the only thing capable of querying it was the instance itself, a threat model that held perfectly well until an application on that instance became willing to ask on someone else's behalf.
Nobody actually owns the boundary that failed. The platform or infrastructure team that provisioned the VPC and decided the metadata endpoint didn't need a credential was solving a different problem — bootstrapping instance identity without a chicken-and-egg credential distribution issue — and had no visibility into which application features, built by different teams, possibly years later, would introduce a URL-fetching code path inside that same network. The application team building the link-preview feature had no reason to think about cloud metadata endpoints at all; they were solving "make a network call to a user-supplied URL," a feature request with an entirely reasonable-sounding one-line description. Each side made a locally sensible decision. Neither side was positioned to see that the two decisions, stacked, produced a hole — because "is internal-network position still an acceptable substitute for authentication, given everything that now runs inside this network" was never anyone's assigned question to keep re-asking.
Treating network position as implicit authentication lets internal services skip a real engineering cost — building, rotating, and distributing credentials for every service-to-service call inside a trusted perimeter — which is a substantial amount of legitimate complexity avoided, especially early in a system's life.
That shortcut has no natural point at which someone is prompted to revisit it. The perimeter only gets more permeable over time — more application code capable of making arbitrary outbound requests, more services added inside the same network — and nothing about the original decision comes with an expiration date or a scheduled re-check.
This is the general shape the rest of the series returns to, so it's worth being precise about it here, at the start: SSRF is not evidence that developers don't understand networking, or that infrastructure teams made a bad call provisioning the metadata endpoint without a credential. It's evidence that "internal network position implies authorization" is a trust decision distributed across two teams that never talk to each other about it, made at two different times, neither of which was ever handed the job of checking whether it still holds as the system around it grows. The fix that actually works — egress control enforced at the network layer, credentials required even for "internal" calls, treating the perimeter as compromised by default (the operating premise of what's now marketed as zero-trust architecture) — is exactly the fix that requires someone to own the boundary continuously, rather than someone having set it correctly once.
The trust boundary here is the edge between "a request that carries the intent of the code that issued it" and "a request that carries only the network position of the machine that issued it" — and on most infrastructure built before that distinction was explicit, the answer to who owns keeping those two things from being treated as equivalent is still, most of the time, no one.