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.
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.
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
.
For a web cache poisoning attack to be possible, a number of conditions must be true:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: <script>alert(1);</script>
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Accept-Language: <script>alert(1);</script>
HTTP/1.1 200 OK
(...)
<h1>Stats Page 1</h1>
<p>Language: <script>alert(1);</script></p>
(...)
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Cache-control: no-store
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
Cache-control: public
Accept-Language: <script>alert(1);</script>
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:
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com:957
GET https://example.com/stats.php?page=1 HTTP/1.1
Host: example.com
?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.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.
The best way to detect vulnerabilities that make web cache poisoning attacks possible depends on whether they are already known or unknown.
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:
For web cache and network administrators:
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.
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.
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