Cache poisoning through a header left out of the cache key
The CDN left a header out of the cache key and the origin reflected it. One poisoned request then reached every visitor who came after.
Summary
The origin built the absolute URLs of its assets from a forwarding header sent by the client. The CDN did not include that header in the cache key: two requests differing only in that header were treated as identical.
An attacker could combine the two faults and leave a copy of the page in the cache, with its asset references pointing at a domain of their own. That copy was then served to every visitor reaching the node.
Technical details
The header was reflected in the response
The origin copied that value into the document’s absolute URLs without validating it:
GET / HTTP/2
Host: [REDACTED]
X-Forwarded-Host: [CONTROLLED DOMAIN]
HTTP/2 200 OK
<script src="https://[CONTROLLED DOMAIN]/assets/app.js"></script>The header was not part of the key
We confirmed that the response was stored and served to requests that no longer carried the header:
GET / HTTP/2 # without the header
HTTP/2 200 OK
X-Cache: HIT
Age: 47
<script src="https://[CONTROLLED DOMAIN]/assets/app.js"></script>
[!] The poisoned response is served to other visitorsExposure window
The configured lifetime determined how many visitors received the poisoned version before it expired:
Cache-Control: public, max-age=600
Vary: Accept-Encoding # the forwarding header is absent
[+] Ten minutes per node, per poisoning
[+] Renewable with a periodic requestWe pointed the header at a domain the team owns, which served no files. The check ran on a single edge node agreed with the client. We purged the poisoned entry when we finished.
Impact behind the 8.3 rating
- Scripts loaded from an attacker-controlled domain in any visitor’s browser on the affected node
- Session theft from any authenticated user who loads the page
- Spread across the node: one request reaches every visitor who arrives afterwards, until the entry expires
- Brand impersonation: the poisoned page is served from the legitimate domain
- Delayed detection: requests served from cache do not appear in the origin’s logs
Remediation
- Do not build absolute URLs from client headers. Use the canonical address configured on the server.
- Key the cache on every header that changes the response, or stop accepting those headers at the edge.
- Reject unexpected forwarding headers at the CDN, before they reach the origin.
- Declare
Varycorrectly for headers that legitimately differentiate responses. - Purge the cache after deploying the fix: entries that are already poisoned keep being served until they expire.
References
More write-ups
Check whether this vector affects your systems
Tell us which assets you want tested. We reply within 24 hours.