Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Web Cache Poisoning — Parameter Cloaking, Inconsistent Parsing
Difficulty Practitioner
Objective Use the parameter cloaking technique to 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

Parameter Cloaking

Intercepting / showed the usual response plus a script import worth noting:

GET / HTTP/2
Host: 0ad500060344d5c4808503e80070009a.web-security-academy.net
Cookie: country=[object Object]
<script type="text/javascript" src="/js/geolocate.js?callback=setCountryCookie"></script>

That explains the country=[object Object] cookie — there's a JSONP-style request being made. Checking /js/geolocate.js?callback=setCountryCookie in HTTP history:

Screenshot
Screenshot
const setCountryCookie = (country) => { document.cookie = 'country=' + country; };
const setLangCookie = (lang) => { document.cookie = 'lang=' + lang; };
setCountryCookie({"country":"United Kingdom"});

Classic JSONP — the callback query parameter directly names the function invoked with the geolocation data. In Repeater, changing callback to an arbitrary value confirmed the response follows it:

GET /js/geolocate.js?callback=Teto HTTP/2
Teto({"country":"United Kingdom"});
Screenshot

The response also carries Cache-Control: max-age=35 — it's cacheable. JSONP callback parameters are a high-value injection point: whatever string lands in callback gets executed as a function call verbatim, so controlling it is straightforward code execution rather than just data reflection. The challenge is controlling what callback evaluates to at the backend without changing what the cache sees in the request, so the cache stays keyed on the legitimate-looking callback=setCountryCookie while the backend executes something different.

I ran Param Miner's "Guess query params" to find an unkeyed parameter:

Screenshot

utm_content came up again as ignored by the cache key. Confirmed — requesting / and /?utm_content=teto both served the same cached / response.

Screenshot

The cloaking technique: smuggle a second callback parameter inside the unkeyed utm_content value, using ; as a separator. The cache sees callback=setCountryCookie (since utm_content and everything appended to it is unkeyed), but the backend parses ; as an additional parameter delimiter and resolves duplicate callback values by taking the last one — so the smuggled value wins at the backend while the cache stays oblivious to its presence. This is a step up from plain unkeyed-parameter poisoning: it's not enough that utm_content is unkeyed — the exploit depends on the backend and the cache parsing the same request differently.

GET /js/geolocate.js?callback=setCountryCookie&utm_content=teto;callback=miku HTTP/2
Screenshot
miku({"country":"United Kingdom"});

The smuggled callback=miku won at the backend. Swapping it for the actual payload:

GET /js/geolocate.js?callback=setCountryCookie&utm_content=teto;callback=alert(1) HTTP/2

After a few attempts to get the timing right, the alert fired and the lab was solved.

Screenshot

Lab solved 9.9

Resources