Application Security
cache poisoning
cdn
cloudflare
web security
+3 more

Web Cache Poisoning: How Attackers Weaponize CDNs and Reverse Proxies

SCRs Team
February 17, 2026
14 min read
Share

What Is Web Cache Poisoning?

Web cache poisoning tricks a caching layer (CDN, reverse proxy, load balancer) into storing a malicious response and serving it to all subsequent visitors.

Normal flow:
  User → CDN Cache (cached /page) → 200 OK (clean page)

Poisoned flow:
  Attacker → Origin Server → 200 OK (malicious response injected via headers)
         ↓
  CDN Cache stores poisoned response
         ↓
  ALL subsequent visitors → CDN Cache → 200 OK (malicious page!)

Impact: One request from an attacker → XSS/defacement/phishing served to every visitor until cache expires.


How Cache Poisoning Works

The attack relies on unkeyed inputs — parts of the request that the cache ignores when generating the cache key, but the origin server uses in the response.

Cache Key vs. Full Request

Full Request:
  GET /page HTTP/1.1
  Host: target.com
  X-Forwarded-Host: evil.com     ← Unkeyed (not in cache key)
  Accept-Language: en              ← Unkeyed
  Cookie: session=abc123           ← Usually unkeyed

Cache Key (what CDN uses to match requests):
  GET target.com/page
  
The CDN sees the same "page" for all visitors, regardless of 
X-Forwarded-Host or Accept-Language headers.

Attack 1: X-Forwarded-Host Header Poisoning

Many frameworks use X-Forwarded-Host to generate absolute URLs for assets, links, and redirects.

# Attacker's request
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com

# Origin response (using X-Forwarded-Host for asset URLs)
<html>
  <script src="https://evil.com/assets/main.js"></script>
  <link rel="stylesheet" href="https://evil.com/assets/style.css">
</html>

# CDN caches this response for the cache key "GET target.com/"
# All visitors now load JavaScript from evil.com!

Defense

// ✅ Never use X-Forwarded-Host without validation
const ALLOWED_HOSTS = ['target.com', 'www.target.com'];

function getHost(req: Request): string {
  const forwarded = req.headers['x-forwarded-host'];
  if (forwarded && ALLOWED_HOSTS.includes(String(forwarded))) {
    return String(forwarded);
  }
  return req.headers.host || ALLOWED_HOSTS[0];
}

// ✅ Better: Use a fixed base URL from config
const BASE_URL = process.env.BASE_URL; // https://target.com

Attack 2: Unkeyed Header Reflection

# Find headers reflected in response
GET / HTTP/1.1
Host: target.com
X-Custom-Header: <script>alert(1)</script>

# If the response includes:
# <meta name="custom" content="<script>alert(1)</script>">
# And the CDN caches it — stored XSS for all visitors

Testing Methodology

# Step 1: Find unkeyed headers using Param Miner (Burp extension)
# Or manually test common headers:
X-Forwarded-Host
X-Forwarded-Scheme
X-Original-URL
X-Rewrite-URL
X-Forwarded-Proto
X-Host

# Step 2: Check if header value appears in response
# Step 3: Verify the response gets cached (check Age, X-Cache headers)
# Step 4: Confirm other users get the cached poisoned response

Attack 3: Fat GET / POST Body Poisoning

Some caching systems treat GET requests with a body the same as regular GETs.

# Cache key: GET /api/user (no body considered)
GET /api/user HTTP/1.1
Host: target.com
Content-Length: 30

{"role": "admin", "id": "1"}

# If the origin uses the body to customize the response,
# the cache stores the admin response for /api/user
# All users now see admin data!

Attack 4: Cache Key Normalization Exploits

# Some CDNs normalize URLs before generating cache keys:
# /page and /PAGE might share the same cache key
# /page and /page? might share the same cache key

# But the origin server might treat them differently,
# allowing you to poison the normalized version

Detection: Check Your Cache Headers

# Look for these response headers to understand caching behavior
curl -sI https://target.com | grep -i 'cache\|age\|x-cache\|cf-cache\|vary'

# Key headers:
# Cache-Control: max-age=3600        → Response is cached for 1 hour
# Age: 542                           → Response has been cached for 542 seconds
# X-Cache: HIT                       → Response served from cache
# Vary: Accept-Encoding              → Cache key includes Accept-Encoding
# CF-Cache-Status: DYNAMIC           → Cloudflare not caching (safe)

CDN-Specific Defenses

Cloudflare

# Cache Rules → Only cache static assets
# Page Rules → Cache Level: Bypass for dynamic pages
# Transform Rules → Strip unrecognized headers before origin

CloudFront

{
  "CacheBehavior": {
    "ForwardedValues": {
      "Headers": ["Host"],
      "QueryString": true
    },
    "CachePolicy": {
      "HeadersConfig": {
        "HeaderBehavior": "whitelist",
        "Headers": ["Host", "Accept-Encoding"]
      }
    }
  }
}

Cache Poisoning Prevention Checklist

  • Use fixed base URLs — never generate URLs from request headers
  • Strip unknown/custom headers at the CDN/proxy level
  • Ensure Vary header includes all inputs that affect the response
  • Don't cache responses that vary based on cookies or custom headers
  • Cache only static assets — bypass cache for dynamic/personalized content
  • Set Cache-Control: private, no-store for authenticated responses
  • Monitor cache HIT rates for anomalies
  • Test with tools like Param Miner to find unkeyed inputs
  • Use Surrogate-Control or CDN-Cache-Control for CDN-specific cache rules

Advertisement