Skip to content
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.

Screenshot
Screenshot

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
Screenshot
Screenshot

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:

Screenshot

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:

Screenshot
<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>'/>
Screenshot

The lab was marked solved before even reloading in the browser — the automated check picked up the poisoned cache state directly.

Screenshot

Lab solved :P

Resources