Web Cache Poisoning

Web cache poisoning is an online attack in which an attacker exploits vulnerabilities in a web server and its associated caching server to cache and serve a malicious HTTP response to other users of the web server. That’s certainly a mouthful, but we’ll unpack that. In this article, we will explain what web cache poisoning is, how it works, and what you can do about it.

What is a web cache?

To understand web poisoning attacks, we first need to understand what a web cache actually is.

Whenever you make a request to a web server, through your browser, the web server would normally respond with a new response for every request. And the web server would search for and serve the requested resource to the user. Without an intermediary caching server between the user’s device and the server, the server would fetch the requested resource and serve it to the user in a new response each time that resource was requested. On a busy website, this would quickly become unmanageable and would overload the server.

But with a caching server sitting between the user and the server, certain responses can be cached by the caching server and served to the user from there (the caching server) rather than having the web server fetch that resource again and again. This greatly reduces the load on the web server. In fact, responses served from the caching server don’t even require interaction with the actual web server.

Cache keys

The caching server will cache responses based on a number of factors, such as the file extension of the requested resource, the content type, the route, the status code, and the response header. This is determined by the site administrator.

When the cache server receives an HTTP request, it needs to determine if there’s a cached response available or whether it needs to forward the request to the backend server. The caching server will compare a predefined subset of parameters from the request’s headers to identify equivalent requests. This subset of parameters is called the cache key.

The parts of the request that aren’t included in the cache key are considered “unkeyed” parameters. These unkeyed parameters are ignored by the caching server altogether. This latter point is crucial to mounting a successful cache poisoning attack, as we’ll see further down.

If the cache key of two incoming requests matches, the caching server considers these requests as being equivalent and will serve the same response to both requests from the cache. And this will continue to happen for all matching requests until that request’s cache expires.

What can happen if you’re hit with a web cache poisoning attack?

The impact of a web cache poisoning attack is difficult to delineate. That’s because the impact of the attack essentially depends on two factors:

What the attacker was able to get cached successfully

Based on the web server’s configuration, an attacker may successfully cache an infected file, a malicious script, a malicious link, or some other exploit. Any one of these attack vectors could be used to compromise your personal information, download malware onto your machine, or perform a range of other attacks.

Also, bear in mind that a web poisoning attack can be used alongside other attacks and do even more damage than if it were used in isolation.

The volume of traffic on the affected page(s)

If nobody visits the compromised page(s), then nothing will happen. The poisoned response will be served to users making requests to the compromised page(s) while the cache is poisoned. Based on the popularity of the pages in question, the aftermath can range from no impact to disastrous.

And while a cache entry does expire, a seasoned attacker can script their attack to re-poison the cache indefinitely. Making sure your web cache expires after a fixed duration is still something you should be doing, but it’s not guaranteed to thwart a web cache poisoning attack. We’ll look at how to protect against web cache poisoning attacks further down.

How do web cache poisoning attacks work?

A typical web cache poisoning attack comprises three basic steps:

  1. Find the unkeyed inputs
  2. Generate a malicious response from the web server
  3. Get the malicious response cached

Finding unkeyed inputs

Web cache poisoning attacks are based on the manipulation of unkeyed inputs, such as headers. Web caches will ignore unkeyed inputs when evaluating whether or not to serve a cached response.

This means you can use unkeyed inputs to inject your malicious payload and generate a “poisoned” response. This poisoned response, if cached, will be served to all requests matching the cache key. So the first step in every web cache poisoning attack is to identify the unkeyed inputs that are supported by the server.

An attacker may manually identify unkeyed inputs by adding random inputs to requests and then waiting to see if they affect the server’s response. But this will be tedious and time-consuming. There are software tools that can automate this process, and these tools are what make this attack practical.

Generating a malicious response from the web server

From there, with some known unkeyed inputs, the attacker needs to figure out how the web server processes them in order to get the web server to generate a malicious response.

The attacker will look for vulnerabilities to exploit, such as the web server reflecting user input in the response from the server without it being properly sanitized or dynamically generating content based on user input. These are the kinds of misconfigurations that enable web cache poisoning attacks, along with many others.

Getting the malicious response cached

From here, the attacker needs to get their malicious payload cached. They know, by this stage, how to generate a malicious response from the server, but they need to fool the caching server into accepting their payload into the cache.

The attacker will attempt to figure out the caching logic through trial and error, noting how the cache behaves at every step. With any luck, they’re able to cache their payload onto the cache server. We’ll provide a detailed example below. And from there, anyone requesting the same resource will receive a harmful response from the poisoned cache.

Detailed examples of web cache poisoning attacks

Delivering an XSS scripting payload using web cache poisoning

The easiest way to pull off a web cache poisoning attack is to exploit unkeyed input that’s reflected in a cacheable response without being properly sanitized.

Let’s look at what a typical request and its corresponding response could look like:

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

In the above example, the X-Forwarded-Host header value is used to dynamically generate an Open Graph image URL, which gets reflected in the response. In many cases, X-Forwarded-Host headers are considered unkeyed values. In our example, the cache could be poisoned by generating a response containing a simple XSS payload:

GET /en?region=uk HTTP/1.1
Host: legit-website.com
X-Forwarded-Host: a."><script>evil-script</script>"
HTTP/1.1 200 OK
<script src="https://evil-site.com/static/evil-script.js"><script>

If this response were to be cached, any user accessing /en?region=ca would be served this XSS payload instead of the legitimate resource. And then fun things start happening to you…

Exploiting improper handling of resource imports to mount a web cache poisoning attack

Some websites use unkeyed headers to dynamically generate URLs for importing resources, such as JavaScript files, from an external server. In this scenario, if the server is not properly configured, an attacker may change the value of the appropriate header to a domain under their control. And they could manipulate the URL to point to their own malicious JavaScript file instead.

Suppose the attacker manages to cache the response containing this malicious URL. In that case, the attacker’s JavaScript will be imported and executed in the browser of any user making a request with a matching cache key.

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

The PayPal example

In 2017, an Israeli security researcher named Omer Gil uncovered that PayPal was vulnerable to web cache poisoning attacks. He discovered that by attempting to access non-existent resources from the PayPal website—a CSS or a JavaScript file, for example—the cache server would provide responses that included other users’ personal information. The compromised user information ranged from email addresses, account balances, the last four digits of their credit card, and more.

How to prevent web cache poisoning attacks

There is one surefire way to prevent web cache poisoning attacks, and that’s to disable caching altogether. This likely won’t be feasible for larger web sites, but some smaller sites could implement this solution. For example, if caching is only enabled because that’s the default for the CDN you’re using, you may want to think about whether you really need caching enabled or not. If you don’t, disable it.

If your web site/application does indeed require caching, here are some guidelines you should follow:

Only cache static files

You want to enable caching only for purely static files that never change and that don’t depend on any user input to generate a cached response. This way, an attacker won’t be able to fool your caching server into retrieving a malicious version of a file rather than the intended legitimate file.

Be wary of third-party software

Many, if not most, web sites/applications today are built using some third-party software. Your internal development team may hold itself to a high standard in regards to security practices when coding.

But, as soon as you introduce third-party software into the mix, you’re implicitly relying on the robustness of that third-party development team’s security practices. If they’re weaker than yours, that third-party code becomes your weakest link, and your web site/application is as vulnerable as that code.

Don’t rely on or trust data found in HTTP headers

Many client-side vulnerabilities can be exploited through HTTP headers, which can lead to reflected cross-site scripting attacks, among other attacks.

A reflected XSS attack is possible when a web site/application accepts user input and reflects the results back to the user (such as a search field) but without validating the input. It simply reflects whatever was input by the user. In our example above, the user’s language preference was used.

So don’t rely on values from HTTP headers that aren’t part of your cache key. And never return HTTP headers from the web cache.

Don’t trust GET requests

You should consider GET request bodies as untrusted, and you should make sure GET request bodies cannot modify the contents of a response. If a GET request body is able to change the contents of a response, consider using a POST request instead or bypassing the cache altogether.

Conduct regular vulnerability testing

Run vulnerability analysis on your web site/application at regular intervals to make sure it isn’t vulnerable to cross-site scripting attacks in particular. Regular vulnerability analysis will also protect you from other online threats.

Wrap-up

While only disabling caching altogether can guarantee that you’re not hit with a web cache poisoning attack, the proposed measures above will considerably skew the odds of not falling victim to this attack in your favor. Any site/service that lives online is living in a hostile environment. And without proper defenses, it won’t live very long. So every little bit helps.

As always, stay safe.

See also: