Web cache poisoning
What is web cache poisoning?
Web cache poisoning happens when an attacker tricks a web cache into storing a malicious HTTP response from a vulnerable web application or web API. The malicious reply is then served to everyone accessing the cached web resource, until the cached value expires.
What is a web cache?
The function of a cache in computer systems is to speed up response times. Whenever a server response to a specific client request is always the same, the reply can be stored in a cache as a static copy and served to other clients directly from the cache, without involving the server at all. This improves response times and frees server resources to let the backend work more efficiently on other requests. The same principle is used in web technology to build web caches.
In the web ecosystem, there are many types of web caches at different points of the network. A cache may be located next to a web server and support only that server, but you can also have a major cache at the content delivery network (CDN) level or – at the other extreme – a client-side web browser cache serving only one user. Web caches differ in scope (number of websites and web applications served and number of users using the web cache), the software used (for example, Memcached, Varnish), and the specific configuration, but they all work in basically the same way.
What is a web cache key?
The main challenge for a web cache is deciding whether a request is similar enough to one that already has a cached response. The cache software needs to decide whether to pass the request to the web server or immediately serve a cached response to the requesting client. To make the comparison, web caches usually use a cache key.
The cache key is a selected set of HTTP request elements (parts of the request line and the headers) and their values. If all the values in a cache key match those of a previous request, the cache assumes it can return the cached response associated with that request, without the need to get a new response from the web server. Parts of the HTTP request that are included in the cache key are called keyed inputs, and the rest are unkeyed inputs. Almost all cache keys include at least the path and host, but other header values may also be used. Sometimes, only selected parts and parameters of the path are keyed, rather than the entire path.
In this example, the web cache uses the entire request path and the Host header as the cache key. The initial request to the web server is as follows:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Accept-Language: en-US
The web server responds with a web page, which will look something like:
HTTP/1.1 200 OK
(...)
<h1>Stats Page 1</h1>
<p>Language: en-US</p>
(...)
The response (i.e. the entire web page) is now cached and will be served to other clients requesting the same resource. When the following request comes in, the cache needs to decide whether to send it to the web server or to reply with the cached response:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Accept-Language: mt
Since this cache only cares about the path and the Host header value, it assumes it can send the cached response. However, in this case, you can see that the cache key was not sufficiently accurate – the user requested a Maltese version of the page (the request includes Accept-Language: mt
) but was served the English language version instead because the cached response was for a request with Accept-Language: en-US
.
When is web cache poisoning possible?
For a web cache poisoning attack to be possible, a number of conditions must be true:
- The cached web application must be vulnerable to attacks such as cross-site scripting or Host header attacks performed via HTTP request headers (such as X-Forwarded-Host, User-Agent, Content-Type, or another unkeyed header). In other words, the application needs to accept an attacker-supplied value without sanitization and return it in the response. Web cache poisoning is not possible unless the application is already vulnerable to other attacks.
- The web cache must not include the vulnerable HTTP header in its cache key. To see why, let’s assume the Host header is included in the key, and the original request with a cached response was:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: <script>alert(1);</script>
- This resource would never be served from the cache unless a victim sent a request with the same Host header value, which can never be the case. However, if the malicious content is included in unkeyed headers like Accept-Language, the same content will be served for any Accept-Language values. For example, the following request:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Accept-Language: <script>alert(1);</script>
- will receive the following response from the cache:
HTTP/1.1 200 OK
(...)
<h1>Stats Page 1</h1>
<p>Language: <script>alert(1);</script></p>
(...)
- The attacker needs to find a resource that is not yet cached, perform the attack at a time when the cached legitimate resource is about to expire, or have the ability to clear the currently stored cache entry forcefully. For example, if the attacker first sends the following request:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Cache-control: no-store
- a vulnerable cache server may assume that the https://example.com/stats.php?page=1 resource is not to be cached and remove the current response value from the cache. The attacker’s next request could then be:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Cache-control: public
Accept-Language: <script>alert(1);</script>
- causing the cache to store a malicious response.
Types of web cache poisoning attacks
In addition to taking advantage of unkeyed HTTP headers, there are also other ways to perform practical web cache poisoning attacks, with a variety of potential consequences:
- If the port number is not part of the cache key, an attacker may send a request to an inaccessible port, for example:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com:957
- Since the web server does not respond on port 957, it returns an error page, which is cached. The port number is not part of the key, so this error page will now be returned even for user requests sent to the regular HTTP port number (default 80):
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
- This makes the path inaccessible to valid users, in practice resulting in a denial-of-service (DoS) attack against that path.
- If the request method is not part of the key, an attacker may be able to send a POST request that modifies a parameter. The response would be cached and then served to clients requesting the same path using the expected GET method.
- If the query string (e.g.
?page=1
) is not part of the key, it may be possible for an attacker to change a reflected cross-site scripting attack into stored cross-site scripting by caching the XSS payload.
Example of web cache poisoning
In the following example, the web application uses the unsanitized Accept-Language header value in the HTML response body:
<?php
(...)
$page = $_GET["page"];
$language = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
$response = "<h1>Stats Page $page</h1>\r\n<p>Language: $language</p>";
(...)
An attacker could send the following request that includes an XSS payload:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Accept-Language: <script>alert(1);</script>
Since the web application does not sanitize the value of the Accept-Language header, the HTTP response will be as follows:
HTTP/1.1 200 OK
<h1>Stats Page 1</h1>
<p>Language: <script>alert(1);</script></p>
If a response to a matching request was not stored in the cache before, the cache now stores the poisoned response and serves it to anyone requesting https://example.com/stats.php?page=1, resulting in a persistent cross-site scripting (XSS) attack.
How to detect web cache poisoning vulnerabilities?
The best way to detect vulnerabilities that make web cache poisoning attacks possible depends on whether they are already known or unknown.
- If you only use commercial or open-source web applications, it may be enough to identify the exact version of the application you are using. If the identified version is known to be susceptible to web cache poisoning attacks, you can assume that your installation is vulnerable. You can identify the web app version manually or use a suitable security tool, such as a software composition analysis (SCA) solution.
- If you develop your own web applications or want the ability to potentially find previously unknown vulnerabilities that lead to web cache poisoning attacks (zero-days) in known applications, you must be able to successfully exploit the vulnerability to be certain that it exists. This requires either performing manual penetration testing with the help of security researchers or using a vulnerability scanner tool that can automatically exploit web vulnerabilities. Examples of such tools are Invicti and Acunetix by Invicti. We recommend using this method even for known vulnerabilities.
How to prevent web cache poisoning attacks?
The simplest way to eliminate the risk of web cache poisoning would be not to use web caches at all, but in today’s infrastructure, this is not possible. The following best practices will help you prevent or mitigate attacks performed via web caches:
For web application developers:
- Make sure your web application is not susceptible to Host header attacks or other attacks made possible by using unsanitized user input (including HTTP header values) in HTTP responses.
- Eliminate any cross-site scripting vulnerabilities in your web applications, since web cache poisoning may allow an attacker to extend the reach of an XSS attack.
- Do not generate any part of the response, including response headers, on the basis of HTTP request header content without careful sanitization.
- Follow general web security best practices. In particular, never trust any input that may be manipulated by the user.
For web cache and network administrators:
- Configure your web cache to remove the port number from the Host header before generating the cache key. This prevents DoS attacks using an unkeyed port value.
- Configure your web cache to only cache responses to GET and HEAD requests, and never cache any responses to POST or other request types. By design, POST requests are not meant to result in cacheable responses, and this practice reduces the risk of parameter pollution via web cache poisoning.
- Do not allow GET requests with a body. Configure your web cache to reject such requests.
- Understand and restrict where caching is done. Are you using frameworks that implement their own caching? If so, you may want to disable framework-level caching and rely on caching by a content delivery network such as Cloudflare.
- If possible, restrict caching only to content that is definitely static, such as PNG files, standalone JavaScript scripts, etc.
Frequently asked questions
What are web cache poisoning attacks?
Web cache poisoning attacks happen when a malicious hacker tricks a web cache into storing a malicious response from a vulnerable application. If the attack is successful, the web cache will then deliver the malicious response, such as a cross-site scripting payload, to everyone requesting the cached resource.
How dangerous are web cache poisoning attacks?
Web cache poisoning itself is only a way to deliver malicious payloads to unsuspecting users or to perform DoS attacks. It is as dangerous as the vulnerability that is exploited through web cache poisoning, such as XSS or Host header injection. The real danger is that any successful attack may silently affect all clients that access the poisoned cache.
Read about Host header attacks, which are closely related to web cache poisoning.
How to prevent web cache poisoning attacks?
To prevent web cache poisoning attacks as a web application developer, you should focus on eliminating all vulnerabilities that could be exploited through a poisoned web cache, such as XSS. To prevent web cache poisoning as a web cache administrator, you should strip port numbers before generating keys, allow caching only for GET and HEAD requests, and reject all GET requests with a body.
Watch a Paul’s Security Weekly episode on web cache poisoning with Invicti’s Timur Guvenkaya
Related blog posts
Written by: Tomasz Andrzej Nidecki, reviewed by: Benjamin Daniel Mussler