# Cache poisoning via parser mismatch

Cache key built from raw request while origin decodes cookies, URI length, or headers differently

_Area: Web infrastructure and server-side requests. Seen in 2 disclosed reports; one confirmed bounty: $500; latest 2024-06-03. Updated 2026-11-02._

## How it works
A cache stores a response under a key computed from the raw request (path, query, sometimes selected headers). The origin server, however, parses the same request with different rules: it decodes quoted cookies, counts characters after decoding, or errors on a header combination. When the two interpretations diverge, an attacker can make the origin return an error or attacker-influenced body for a request that the cache treats as identical to a legitimate one. The poisoned response is then served to everyone hitting that key. The broken assumption is that cache and origin agree on what a request *is*.

## Where it shows up
- CDN or reverse proxy in front of an origin that uses legacy parsers (Tomcat/Spring RFC2109 cookies, quoted strings, backslash/octal escapes) [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-bypassing-wafs-with-the-phantom-version-cookie.md)
- Length limits applied after URL-decoding, where multi-byte UTF-8 expands to many encoded bytes [E2](https://bugbountylibrary.com/r/h1%3A350847.md)
- Reflected input in cached responses, especially where a NULL byte truncates the reflected value [E2](https://bugbountylibrary.com/r/h1%3A350847.md)
- Backends that return 404/5xx on malformed or unusual header sets, with the CDN caching that error [E3](https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md)
- Registries and artifact hosts serving tarballs/packages through a shared cache [E3](https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md)

## How to review
Identify the exact cache key (which parts of the request, which headers, any normalization). Then identify the origin's parser and its limits. The invariant that must hold: **any two requests the cache considers identical must produce the same response from the origin, and error responses must not be cached under keys reachable by clean requests.** Check that length limits are enforced on the raw bytes the origin actually receives, not on a decoded form [E2](https://bugbountylibrary.com/r/h1%3A350847.md). Check that malformed-header errors are not cacheable [E3](https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md). Check whether the origin honors legacy cookie syntax that a WAF or cache does not decode [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-bypassing-wafs-with-the-phantom-version-cookie.md).

## How finders spotted it
- Compared how Flask, Django, PHP, Ruby, Spring and SimpleCookie parse the same Cookie header, noticed divergent handling of `$Version`, quotes and escapes, then tested against WAF signatures [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-bypassing-wafs-with-the-phantom-version-cookie.md)
- Knew a prior fix limited URI length to 1024 characters after decoding, hypothesized multi-byte chars expand to up to 12 encoded bytes, and confirmed with repeated characters [E2](https://bugbountylibrary.com/r/h1%3A350847.md)
- Ran a CPDoS test tool against npmjs.com as a sanity check, got a hit first run, and confirmed it was not a false positive [E3](https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md)

## Variants seen
- Cookie-parser differential to smuggle WAF-blocked payloads [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-bypassing-wafs-with-the-phantom-version-cookie.md)
- Decode-then-limit bypass via multi-byte UTF-8 amplification [E2](https://bugbountylibrary.com/r/h1%3A350847.md)
- Malformed-header error cached as 404 for a package tarball [E3](https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md)

## Why fixes fail
E2 is an incomplete fix: the 1024-character limit was applied after decoding, so multi-byte characters still expanded past the server's byte limit and re-enabled poisoning [E2](https://bugbountylibrary.com/r/h1%3A350847.md).

## Impact and severity
E1 bypassed WAF filtering entirely. E2 caused DoS by poisoning cached embed JS. E3 denied package installs to all users; targeting high-download packages like express amplified supply-chain impact, and pre-poisoning before a release could break fresh installs [E3](https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md). Severity rises with cache reach and how central the poisoned resource is.

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-bypassing-wafs-with-the-phantom-version-cookie.md
- E2: https://bugbountylibrary.com/r/h1%3A350847.md
- E3: https://bugbountylibrary.com/r/blog%3Alandh-tech-blog-20240603-npm-cache-poisoning.md
