Web Cache Poisoning
Useful Resources & Tools
Introduction to Web Cache
Web caches are commonly used in the deployment of web applications to improve performance and reduce load on backend servers. Content Delivery Networks (CDNs) and reverse proxies are examples of web caches that sit between the client and the web server, serving cached content directly to users instead of forwarding every request to the origin server.
When a client requests a resource that is not already stored in the cache, the cache retrieves it from the web server and stores it locally. Subsequent requests for the same resource can be served directly from the cache, reducing latency and server load. Cached content is typically stored for a limited period, ensuring server updates are propagated to users.
Web caches typically store static resources such as stylesheets and JavaScript files. However, depending on configuration, they may also cache dynamic responses generated from user input, such as search results. If not handled carefully, caching dynamic content can introduce security risks.
To decide whether two requests can be served the same cached response, web caches rely on a cache key. The cache key is a subset of request attributes used to uniquely identify a resource.
By default, this often includes the request path, query parameters, and the Host header, although cache key composition can be customized to include or exclude additional headers or parameters
Web cache poisoning attacks exploit misconfigurations in cache key handling: by manipulating parts of a request that are not included in the cache key, an attacker can cause the cache to store a malicious response. This poisoned response may then be served to other users, enabling attacks such as reflected cross-site scripting (XSS) without requiring direct user interaction.
Note: Web cache poisoning is generally an amplifier of existing vulnerabilities: it acts as an exploitation technique that increases the impact of existing issues such as reflected XSS or Host header vulnerabilities.
Initial Enumeration
Remember: all parameters that are part of the cache key are called keyed parameters. All the others are named unkeyed parameters
The first step in identifying web cache poisoning vulnerabilities is identifying unkeyed parameters that we can use to inject a malicious payload into the response.
For a cache poisoning attack to be effective, the injected payload must originate from an unkeyed parameter. If a parameter is keyed, its value must remain identical when a victim later requests the resource, otherwise the cache entry will not be reused.
Unkeyed request parameters (path, GET parameters, HTTP headers) can be identified by observing whether a cached or fresh response was served.
To find whether a web application is potentially vulnerable to web cache poisoning attacks, you need to iteratively try to find a way to cache your request. Then, it is required to find the unkeyed parameters to inject in the cached response to deliver payloads for attacks such as XSS.
Similar to unkeyed GET parameters, it is quite common to find unkeyed HTTP headers that influence the response of the web server, which can potentially help delivering payloads.
Cache Busters
In real-world engagements, we must ensure that our poisoned response is not served to any actual users of the web application. We can achieve this by adding a cache buster to all our requests.
A cache buster is a unique parameter value used to guarantee a unique cache key. Since we have a unique cache key, only we get served the poisoned response, and no real users are affected.
A basic example is a web application where the ?language GET parameter is keyed.
In this case, we can use ?language=anythingrandoman123 to make sure no real users are harmed.
Approaching highly visited web applications
In real world applications, due to the amount of users visiting the webapp, your requested resources are probably already cached.
To work around this, you can try to bypass the cache for your own request by sending the Cache-Control: no-cache header. In most default configurations, caches respect this header and forward the request to the origin server instead of serving a cached response.
This allows you to receive a fresh response that includes your injected payload. If it doesn't work, the deprecated Pragma: no-cache header may sometimes have the same effect.
This is only a way to test for yourself, as these headers only affect your request. They do not force the cache to update or replace its stored response.
As a result, the poisoned response is not cached, and other users continue to receive the original cached content.
To actually poison the cache, the existing cache entry must expire. Only then can a new response be stored. This means the attack often relies on timing: the malicious request must reach the server at the moment the cache is ready to store a new entry. In practice, you will have to guess the right timing.
Some web applications may ease the guesswork by giving out information about their cache expiration time via the Cache-Control response header. By inspecting values such as its max-age, you can estimate when the cached entry will expire and send your request accordingly, so that your desidered response is successfully cached.
Easy Cases
In simple cases, the server indicates the cache behavior via response headers such as X-Cache-Status. This header can reveal when a response has been stored in the cache and when a cached response is served.
For example, a value such as X-Cache-Status: HIT typically indicates that the response was retrieved from the cache, while other values may indicate a cache miss or that the response was freshly generated by the origin server.
X-Cache-Status: HIT
The requested item was found in the cache and served directly.
X-Cache-Status: MISS
The item wasn't in the cache, so it was retrieved from the origin server and potentially cached for future requests.
X-Cache-Status: BYPASS
The cache was intentionally skipped, often due to specific configurations or directives (like Cache-Control: no-cache).
X-Cache-Status: EXPIRED
The cached item was old (expired), so the server fetched a fresh copy from the origin.
X-Cache-Status: STALE
The origin server was unreachable, so the system served a stale (outdated) cached version.
In these cases, you can determine whether a parameter is part of the cache key by changing one parameter at a time and observing the header value. If repeating the same request results in a HIT, and changing a parameter causes a MISS, that parameter is keyed. After identifying a keyed parameter, repeat the same process for the remaining parameters to identify which ones are ignored by the cache.
Basic Cache Poisoning Attacks
HTTP cache poisoning can be used to ease the delivery of existing vulnerabilities. Some interesting cases are:
Reflected XSS: you can leverage cache poisoning as a means to send your payload.
Self-XSS: in some cases, you can deliver self-xss payloads via cache poisoning, making them an actual reflected xss. This typically occurs with unkeyed http request headers that are included in the web application's response. The same can happen with cookies that might change the behaviour of the application (for example, a language cache or consent cookie)
Host Header attacks: if the host header is NOT part of the cache key (which is extremely rare), and if there are redirections based off the host header's value, you can redirect users to malicious pages where possible
Leveraging Fat GETs
Fat GET requests are HTTP GET requests that include a request body.
While the HTTP specification does not forbid a body in a GET request, it does not define any semantics for it. As a result, most caches ignore the request body entirely when building the cache key. Some web servers or application frameworks may still parse parameters from the body of a GET request and use them to generate the response.
If a parameter influences the response and is accepted from the body of a GET request, an attacker can move that parameter from the query string into the request body.
Sometimes, the cache ignores the GET request's body, making the parameter unkeyed, potentially enabling web cache poisoning scenarios that would not be exploitable using standard query parameters.
Leveraging Parameter Cloaking
Parameter Cloaking
Parameter cloaking is a technique that creates a discrepancy between how the web cache and the web server interpret request parameters. Specifically, the web cache and the application end up using different parameters to build the cache key and to generate the response.
As a result, the application may process and reflect a parameter that the cache does not consider when caching the response. This mismatch allows attacker-controlled input to influence a cached response without being part of the cache key.
To exploit parameter cloaking, the web cache needs to parse parameters differently from the web server. One example of parameter cloaking is Python's Bottle web framework CVE-2020-28473.
Python's Bottle web framework allows separating query parameters using a ;, causing a difference in the interpretation of the request between the proxy (using its default configuration) and the server.
This results in malicious requests being cached as completely safe ones, as the proxy would not see the semicolon as a separator, and therefore would not include it in a cache key of an unkeyed parameter.
Example Scenario
Imagine an application where the language parameter is keyed (included in the cache key), but a generic parameter random is unkeyed (excluded from the cache key). An attacker can send the following request: GET /?language=en&random=b;language=de
In this case, the cache sees two parameters: language=en and random=b;language=de.
Since random is unkeyed, it generates a cache key based only on language=en.
Bottle sees three parameters: language=en, random=b, and language=de. In many web frameworks, if a parameter is duplicated, the last value takes precedence. Therefore, the server processes the request as language=de and generates a response in German.
Finally, the web cache stores the german response under the cache key for English (en). Any subsequent user requesting the en version of the site will instead receive the "poisoned" de version.