# Cache key normalization mismatch

CDN and origin disagree on path, dot-segment, or cookie normalization, letting one URL's content be cached under another

_Area: Web injection and code execution. Seen in 2 disclosed reports; latest 2023-04-01. Updated 2026-11-02._

## How it works
A cache decides *what* to store and *under which key* using its own parser; the origin decides *what to serve* using a different one. When the two disagree on normalization — dot-segments, encoded slashes, case, extensions, or which cookies/headers are part of the key — the cache stores a response for URL A while the origin generated it for URL B. The attacker picks B (a page with a reflection or self-XSS) and A (a widely-requested cached path), and the poisoned entry is served to everyone who requests A. The broken assumption is "the cache and origin agree on what this URL means." [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md) A second shape: the cache key omits an input the origin reflects (a cookie), so a per-user reflection becomes a shared cached response [E3](https://bugbountylibrary.com/r/h1%3A1760213.md).

## Where it shows up
- CDN cache rules keyed on a path prefix or wildcard (`/Award/*`) while the origin routes by a different rule [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md)
- Dot-segments (`/a/../b`), encoded slashes, trailing dots, case, or extension mutations handled differently by frontend and backend [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md)
- Cookie values reflected into cacheable responses, especially `.js`/`.php.js` endpoints [E3](https://bugbountylibrary.com/r/h1%3A1760213.md)
- Cache keys that ignore cookies or headers the origin echoes back [E3](https://bugbountylibrary.com/r/h1%3A1760213.md)
- Reflection primitives that echo request bytes verbatim: TRACE handlers, debug endpoints, HEAD/GET concatenation [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md)
- Desync primitives (CL.0, HTTP/2 header injection) where a byte-controlled response body is needed [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md)

## How to review
Establish the invariant: **for every request, the cache key the frontend computes must map to exactly the resource the origin serves, and every input the origin reflects must be in the cache key.** Check by:
- Sending `/cached-prefix/../uncached-vuln-path` and comparing what is cached vs. what is served [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md)
- Mutating extensions, case, and encoded characters to map the real cache rule, not the documented one [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md)
- Confirming whether reflected cookies/headers are part of the key; if not, the response must not reflect them [E3](https://bugbountylibrary.com/r/h1%3A1760213.md)
- Treating any verbatim-echo endpoint (TRACE) as a byte-controlled body source when combined with desync [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md)

## How finders spotted it
- Noticed parameter reflection in a script tag, then removed characters from a blocked payload until the WAF passed it; separately mutated extensions and paths to infer the `/Award/*` wildcard, then hypothesized frontend/backend dot-segment disagreement and tested `/Award/../` and `/Job/../` probes [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md)
- Saw a cookie reflected in a `.js` response, tested the WAF, found it only stripped double quotes, and split blocked tokens with quotes while `<`/`>` still formed tags [E3](https://bugbountylibrary.com/r/h1%3A1760213.md)
- After a bare smuggling report was rejected, searched for reflection primitives, noticed the backend answered TRACE and echoed headers verbatim, then combined it with HEAD response concatenation [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md)

## Variants seen
- Frontend normalizes dot-segments, backend does not; self-XSS cached under a different prefix [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md)
- Cookie reflected unescaped into a cacheable JS response; WAF strips only quotes [E3](https://bugbountylibrary.com/r/h1%3A1760213.md)
- TRACE reflection plus desync/response smuggling to synthesize arbitrary cached responses [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md)

## Impact and severity
Each case escalated a non-exploitable primitive into stored XSS affecting all cache users: self-XSS to stored XSS [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md), reflected XSS to account takeover via session leak in `INITIAL_STATE` [E3](https://bugbountylibrary.com/r/h1%3A1760213.md), and smuggling to full app compromise [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md). Impact rose with reach (shared cache) and chaining (WAF bypass, desync, session leak).

## Why fixes fail
WAF character stripping (quotes only) left `<`/`>` intact and was bypassed by token splitting [E3](https://bugbountylibrary.com/r/h1%3A1760213.md); fixing the reflection without aligning cache keys leaves the mismatch exploitable [E2](https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md).

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-trace-desync-attack.md
- E2: https://bugbountylibrary.com/r/blog%3Anokline-github-io-bugbounty-2022-09-02-glassdoor-cache-poisoning-html.md
- E3: https://bugbountylibrary.com/r/h1%3A1760213.md
