| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Web Cache Poisoning — Unkeyed Query String, Canonical Link Injection |
| Difficulty | Practitioner |
| Objective | Poison the home page 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 String¶
I intercepted a request to / and checked what the response reflected.
The response body contained a canonical link tag:
<link rel="canonical" href='//0a40002304151875803080120091006a.web-security-academy.net/'/>
Trying GET /?teto=1 reflected the query string into the same tag:
<link rel="canonical" href='//0a40002304151875803080120091006a.web-security-academy.net/?teto=1'/>
Following up with a plain GET /:
For the next 35 seconds matching Cache-Control: max-age=35, the canonical link still showed ?teto=1. The cache key for / excludes the query string entirely — requests to / and /?anything share the same cache entry, but the query string still gets reflected into the response by the backend. Same underlying pattern as the unkeyed header and cookie labs, just via a different input type.
Since the query string lands unsanitized inside a single-quoted HTML attribute, it's injectable. The canonical <link> tag is an attacker-friendly injection point precisely because it's metadata rather than visible content — easy to overlook compared to more obviously reflected values. Breaking out of the attribute context:
GET /?teto=1'/><script>alert(1)</script>
The resulting markup:
<link rel="canonical" href='//0a40002304151875803080120091006a.web-security-academy.net/?teto=1'/><script>alert(1)</script>'/>
The '/> closes the href attribute and the <link> tag, <script>alert(1)</script> runs as its own element, and the leftover '/> from the original syntax becomes inert trailing text — no need to clean it up. Breaking out of a single-quoted attribute just requires a matching ' followed by closing the tag before injecting the new element.
Reloading the page:
Alert fired. Since the query string isn't part of the cache key, this poisoned response is now served for plain GET / requests too — including to the victim visiting the home page normally.
Lab solved 0.0