# Web Cache Poisoning - Capability to disable/deface the app.██████████.com (A tale of poisoning through the layers of caching)

CloudFront cache key excluded query string, path, and most headers, letting an unkeyed x-amz-server-side-encryption header poison the cached homepage and deface it for all users.

- Payout: $1,000
- Severity: medium
- Program: -
- Credited: Ankit Singh
- Disclosed: 2023-03-03
- Write-up quality: 7/10
- Original: https://github.com/AnkitCuriosity/Write-Ups/blob/main/Web%20Cache%20Poisoning%20-%20Capability%20to%20disable%E2%88%95deface%20the%20app.vulnerable.com%20(A%20tale%20of%20poisoning%20through%20the%20layers%20of%20caching).md
- Library page: https://bugbountylibrary.com/r/blog%3Agithub-com-ankitcuriosity-write-ups-blob-main-web-20cache-20poisoning-20-20capability-20to-20disable-e2-88-95deface-20th
- Tags: domain:web impact:dos impact:integrity kind:bounty platform:blog sev:medium tech:cdn technique:recon vuln:cache-deception vuln:cache-poisoning

**Target:** CloudFront-fronted web app homepage (app.vulnerable.com)
**Root cause:** Cache key excluded query string, path, and all headers except Accept-Encoding, so an unkeyed x-amz-server-side-encryption header reflected into a 400 response was cached and served to all users.
**Steps:**
1. Observe Age/max-age headers to learn cache TTL is 21600s; wait for Age to reach max-age so cache expires.
2. Send `curl --header "x-amz-server-side-encryption: PoC-by-AnkitSingh..." https://app.vulnerable.com/` immediately after expiry.
3. Backend reflects the header into a 400 Bad Request that CloudFront caches.
4. Any user requesting https://app.vulnerable.com/ receives the poisoned 400 with attacker text instead of the 200 page.
**How found:** Researcher noticed query strings and paths were not cache keys (cache HIT on arbitrary paths), then probed headers and found only single-char Accept-Encoding forced a MISS once; used that to confirm 400 responses were cacheable, then timed cache expiry to poison the root path directly.
**Exploitation:** Attacker waits for the 6-hour cache TTL to expire, then sends a single request with an oversized unkeyed x-amz-server-side-encryption header; the reflected 400 response is cached and served to every subsequent visitor, defacing/disabling the app.
**Escalation:** None beyond full-site defacement/DoS for all users until cache expiry.
**Insight:** When query string and path are unkeyed, don't give up: enumerate headers for any that force a MISS (even single-char Accept-Encoding), and check whether error responses (400) are cacheable — then time poisoning to the Age/max-age expiry window to hit the canonical URL.
