| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Web Cache Poisoning — Unkeyed Specific Query Parameter, Canonical Link Injection |
| Difficulty | Practitioner |
| Objective | Poison the cache with a response that executes alert(1) in the victim's browser |
| Note | A user regularly visits the home page using Chrome |
Web Cache Poisoning via an Unkeyed Query Parameter¶
I intercepted / and got Cache-Control: max-age=35. Testing /?teto=1 created its own distinct cache entry rather than reusing the / one.
Unlike the previous lab, the query string here is part of the cache key by default — / and /?teto=1 are treated as separate entries. Each response also reflects a canonical link matching the request path and parameters. Trying to break out of the attribute context directly:
GET /?teto=1'><script>alert(1)</script> HTTP/2
The injection worked and the alert fired for anyone requesting that exact path — but it doesn't affect plain /, since /?teto=1... is its own separate cache key. To poison the / response, the parameter needs to be specifically excluded from the cache key while still being processed and reflected by the backend.
The query string being part of the cache key by default doesn't mean every individual parameter is — sites commonly exclude tracking-style parameters like utm_content and utm_source since they're analytics data irrelevant to the page content itself. I ran Param Miner's "Guess query param" mode, which covers unkeyed individual parameters separately from header guessing:
It found utm_content as a blacklisted (unkeyed) parameter. Testing it:
GET /?utm_content=teto HTTP/2
Then requesting plain / confirmed that utm_content was genuinely unkeyed — the / response reflected the value we sent while sharing the same cache entry:
<link rel="canonical" href='//0a8f000a03ae62ee80ab4ebf00e60025.web-security-academy.net/?utm_content=teto'/>
With the unkeyed input confirmed, I applied the same attribute-escape technique from the previous lab:
GET /?utm_content=teto'><script>alert(1)</script> HTTP/2
The / response now contained:
<link rel="canonical" href='//0a8f000a03ae62ee80ab4ebf00e60025.web-security-academy.net/?utm_content=teto'><script>alert(1)</script>'/>
The lab was marked solved before even reloading in the browser — the automated check picked up the poisoned cache state directly.
Lab solved :P