Server-Side Request Forgery
An app that fetches a URL an attacker controls can reach services the attacker cannot.
Builds on
What you will learn
- -Recognize the app features that turn user input into a server-side request
- -Explain how SSRF reaches loopback and internal-only services
- -Describe why the cloud metadata endpoint made SSRF a credential-theft vector
- -Defend a URL fetcher with allowlists, address checks, and IMDSv2
The last lesson turned an avatar uploader into a foothold, because an upload is a feature the application offers and an attacker bent it past its purpose. -side request forgery is the same move against a different feature. Instead of accepting a file, the application accepts a and goes and fetches it. The bug is not the fetch. The bug is that the fetch happens from the server, and the server lives inside a the attacker cannot reach directly.
Name the actor that does the work. When a user pastes a link into an image-from-URL field, the browser sends that link to the application, and then the application, not the browser, opens the connection. That outbound request starts inside the data center, behind the , with whatever internal and routing the server enjoys. A request that the public internet would drop sails through because it comes from a trusted .
The vulnerable pattern is any feature where attacker input becomes an outbound request. A webhook tester that pings a URL to confirm it is reachable. A PDF generator that renders a page into a document. A link-preview service that fetches a target to build a thumbnail. An XML parser that resolves an external entity. An avatar that takes a URL instead of a file. Each of these lifts a destination out of the request and asks the server to visit it.
The reason this matters is the trust boundary. Firewalls and cloud on one assumption: internal services only ever talk to other internal services. A Redis instance bound to 127.0.0.1 skips because nothing outside the host can reach 127.0.0.1. An admin panel on 8080 has no route from the internet. collapses that assumption by handing the attacker one trusted mouth inside the wall. The server becomes an unwilling proxy, and every request it makes carries the server's own privileges.
Why does SSRF let an attacker reach services a normal external request cannot?
Reaching internal services
The first target of an SSRF is the interface. Addresses like 127.0.0.1 and localhost resolve to the server itself, and services bound there are the ones treat as private: a Redis on 6379, a metrics dashboard on 9090, a database admin console on 8080. A tester walks the ports through the vulnerable and reads how the response body, the , or the timing changes. A connection refused looks different from a service that answers, and that difference maps what listens on a host that was never meant to answer strangers.
From loopback, the next hop is the local . The source IP the server presents, visible in an out-of-band callback, reveals the private range it sits in, something like 10.0.x.x or 192.168.x.x. Other hosts on that range become reachable one request at a time: internal APIs with no auth, a orchestrator's , a cache , a build server. None of them expected a request from the web application, so few of them check who is calling.
$http://127.0.0.1:6379/$http://127.0.0.1:8080/admin$http://192.168.0.10/SSRF turns a server into a scanner and a battering ram against hosts inside someone else's network. Confine every request in this lesson to a lab or an engagement with written . Reaching internal infrastructure through an application the tester does not own is unlawful access, and the fact that the app made the request does not move the liability.
The cloud metadata endpoint
The highest-value destination is not on the corporate subnet at all. On AWS, Azure, and GCP, every virtual machine can reach a link-local address, 169.254.169.254, that answers with instance metadata. Link-local means it is routable only from the instance itself, so cloud providers parked configuration, tags, and, on AWS, the for the attached to the machine at that address. The design assumed nothing but the instance would ever ask.
That assumption is exactly what an SSRF breaks. The original scheme, now called IMDSv1, answered any that reached it, with no and no . An SSRF that could request an arbitrary URL could request the path and read an AccessKeyId, a SecretAccessKey, and a session token belonging to the instance role. Those keys carry whatever the role was granted, so a single fetched URL could become read access on storage buckets, or the ability to spin up new infrastructure. This is the mechanism behind the 2019 Capital One breach, where an SSRF reached the metadata service and lifted role credentials that opened tens of millions of records.
$http://169.254.169.254/latest/meta-data/iam/security-credentials/The distance between a curiosity and a catastrophe is small here. One reading is that the application fetched a strange internal address. The other is that the attacker now holds live cloud credentials scoped to whatever the instance was trusted to do. The metadata is what moves an SSRF from the first reading to the second.
An app on an AWS instance running IMDSv1 has SSRF that reaches 169.254.169.254. What is the direct consequence, and what defense removes it?
Blind SSRF and filter bypasses
Not every SSRF returns the response to the attacker. Many fetchers discard the body and report only success or failure, or say nothing at all. That is blind SSRF, and it is confirmed out-of-band. The tester supplies a URL pointing at a listener under external , a plain HTTP server or a tool like Burp Collaborator, and watches for the callback. When a request for a unique, unguessable path arrives from the target's IP, the fetch is proven even though the application showed nothing on screen. From there, timing differences and error messages stand in for the missing response body.
Naive defenses blocklist the 127.0.0.1 and localhost, which is a leaky filter because the same address wears many costumes. The short form 127.1 collapses to the same loopback. The decimal integer 2130706433 is 127.0.0.1 written as one number. The bracketed [::1] is the loopback. A hostname registered in public DNS can be pointed at 127.0.0.1 so string matching never sees a suspicious literal. And a filter that inspects the submitted URL but follows can be walked past with a 302 from an allowed host to a forbidden one. A blocklist has to enumerate every disguise, and it never finishes.
Defending a URL fetcher
The fix is to invert the logic. A blocklist of bad destinations loses to the disguises above, so a defender builds an allowlist of the exact hosts the feature is permitted to reach. A webhook tester that only ever calls a handful of partner should refuse everything else, with no convenience exception for arbitrary URLs.
An allowlist alone is not enough, because DNS is attacker-influenced. The server must resolve the hostname, confirm the resulting IP is not in a private or link-local range (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, and the IPv6 equivalents), and then connect to that validated IP rather than re-resolving the name. Re-resolving opens a DNS-rebinding window where the name passes the check and then flips to a forbidden address a moment before the fetch.
Redirects need the same discipline. A destination that passes validation can respond with a 302 to 169.254.169.254, so the fetcher must re-validate every hop against the same address checks, or refuse to follow redirects at all. A single unchecked redirect undoes the allowlist.
Restrict the scheme too. A fetcher that hands the raw URL to a general-purpose library may quietly accept file:// to read a local path, or gopher:// to speak line by line to a Redis or SMTP service that expects a plain text . Those schemes turn a simple GET into a way to write commands to internal . Permitting only http and closes the side doors before the address checks even run.
On the cloud side, the instance itself should require . IMDSv2 makes the metadata service demand a session token obtained through a PUT request that also sets a low TTL hop limit, so a plain SSRF GET, and any request forwarded even one hop away, is rejected before it sees a credential. Setting the instance metadata options to require tokens removes the credential path even when an SSRF exists elsewhere in the .
The last control is quiet but load-bearing: the application should not hand the raw fetched response back to the requester. Returning the body verbatim upgrades a blind SSRF into a full internal read primitive. Fetching server-side and then extracting only the specific field the feature needs, an image's pixels or a webhook's status code, starves an attacker of the internal data even when a request slips past the earlier checks.
A URL-fetching feature must reach only three partner domains. Which control best prevents SSRF?
Open the PortSwigger Web Security Academy lab "Basic SSRF against the local server," which is free and built to be attacked. Intercept the stock-check request in Burp, find the parameter that carries a URL, and change its value to http://localhost/admin. The response comes back with the internal admin page that has no public link, which proves the server fetched a destination you chose. For the blind case, load "Blind SSRF with out-of-band detection," point the vulnerable header at a Burp Collaborator address, and watch the DNS and HTTP interactions arrive from the lab server rather than your browser. To see the same effect on hardware you own, run python3 -m http.server 8000 next to a deliberately vulnerable app such as OWASP Juice Shop and aim its URL fetcher at http://127.0.0.1:8000/probe, then confirm the hit in the server .
SSRF works because the server trusts a destination that arrived in the request. The next lesson turns to a deeper version of that same misplaced trust, where the server accepts not a URL but a whole object rebuilt from bytes the attacker supplied, and the act of rebuilding that object can run code.