Exploiting Unsynchronised Clocks
According to data from RIPE, over 40% of computers attached to the Internet have a few seconds of clock drift, which with the right combination of headers, will make an HTTP response unintentionally cacheable.
Like many parts of the HTTP model, caching has been extended and revised multiple times over the years. The result is a confusing set of response header values, which affect the way that the browser may or may-not cache the response.
If you are not already familiar with the browser caching model, I’d recommend reading the Mozilla caching guide, which is a good primer.
For this article, the two interesting bits are the expires header (which explicitly directs the browser to cache), and the last-modified header (which implicitly triggers heuristic caching). Both of these use a standard HTTP date format response.
So, as long as a response does not have a pragma or cache-control header, then the browser will use expires and last-modified if they are present.
In the Red Corner
From an attack point of view, the reason that you should be interested in responses that are unintenionally cacheable, is that you can use them with cache poisoning to leverage vectors that would otherwise be unexploitable.
The expires header normally works by setting the value to a point in the future, which tells the browser it can cache the response until that time. The potential for unintentional caching is introduced when the application attempts to signal that the resource should not be cached, by setting the expires value to the current time (“now”). That’s because, if the server clock is fast, or the browser clock is slow, then “now” is actually a point in the future, and the browser will cache the response. Ooops.
The last-modified header works in a similar way. If the last-modified value is in the past, then the heuristic caching algorithm in the browser may add the response to the cache.
Putting it all Together
What you are looking for are responses that:
do not contain a cache-control or pragma header that explicitly stops the browser caching the response (so cache-control could contain the private parameter, but not the no-store parameter); and
do contain an expires or last-modified header that is set to the same value as the date header.
HTTP/1.1 200 OK
Date: Sat, 10 Feb 2024 07:08:02 GMT
Content-Type: text/html; charset=utf-8
Expires: Sat, 10 Feb 2024 07:08:02 GMT
In the Blue Corner
The expires and last-modified headers are only used by the browser when the cache-control and pragma headers are not present:
always set the cache-control header on every response, to explicitly control whether you want the browser to cache it or not;
if you absolutely must use an expires header to signal that a response should not be cached, set it to a value waaaay in the past; and
do not rely on last-modified alone to signal caching behaviour to the browser, as the heuristic model varies between browsers, and you may not get what you expected.