Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy Web Cache Poisoning
Advanced · 25 min

Web Cache Poisoning

Learn how unkeyed HTTP headers in cached responses let attackers serve malicious content to all users.

1 Poisoning via Unkeyed Headers

Web cache poisoning exploits the difference between how a cache keys responses (decides if a cached version exists) and how the backend application uses request headers in its responses.

The attack:

  1. Attacker sends a request with a malicious header the cache does not key on
  2. Backend reflects the header value in the response (e.g., a script URL)
  3. Cache stores this poisoned response
  4. All subsequent users receive the malicious response

Example using X-Forwarded-Host:

GET /page HTTP/1.1
Host: legitimate.com
X-Forwarded-Host: attacker.com  ← Not in cache key but reflected by backend!

Response (cached):
<script src="//attacker.com/evil.js"></script>

Other commonly unkeyed headers: X-Forwarded-Scheme, X-Original-URL, X-Rewrite-URL, X-HTTP-Method-Override.

The poisoned response is served to all users requesting the same cache key (URL), potentially affecting thousands of users.

2 Cache Key Normalization and Vary Header

Prevent cache poisoning by ensuring all headers that influence response content are included in the cache key, and avoid reflecting unvalidated headers in responses.

Never reflect unvalidated headers:

from urllib.parse import urlparse

ALLOWED_HOSTS = {"myapp.com", "www.myapp.com"}

@app.before_request
def validate_host():
    # Validate X-Forwarded-Host against allowlist
    forwarded_host = request.headers.get("X-Forwarded-Host", "")
    if forwarded_host and forwarded_host not in ALLOWED_HOSTS:
        # Strip the header — don't reflect it
        del request.environ["HTTP_X_FORWARDED_HOST"]

Vary header for cache keying:

Vary: Accept-Encoding, Accept-Language
# Include any headers your app uses in its response!
# If you use X-Forwarded-Host, add it to Vary:
Vary: X-Forwarded-Host

Cache-Control for sensitive endpoints:

Cache-Control: no-store, no-cache, private
# Prevents caching of user-specific or security-sensitive responses

Knowledge Check

0/3 correct
Q1

What makes a header "unkeyed" in the context of web cache poisoning?

Q2

What does the Vary response header tell a cache?

Q3

What Cache-Control directive prevents a response from being stored by shared caches?

Code Exercise

Prevent Header Reflection

The app reflects the X-Forwarded-Host header in generated URLs without validation. Fix it to only use the header value if it matches an allowlist of trusted hosts.

javascript