Don’t let your cookie being cached by accident!

Cookie caching can causes tricky issues which can be very difficult to troubleshoot because symptoms may vary depending on the purpose of the cookie. In best case, having a cookie cached and distributed to multiple users may not produce any obvious symptoms. In worst case, having an (authentication) cookie distributed to multiple users may bring obvious security concerns… If you are familiar with cookie related issues, you're probably aware of this KB article: An ASP.NET page is stored in the HTTP.sys kernel cache in IIS 6.0 when the ASP.NET page generates an HTTP header that contains a Set-Cookie response. While the title of the article specifies IIS6, there are still situations where cookie can be cached by IIS7 or 7.5 (IIS7+). Let's illustrate this using a simple application that set a cookie on static content (ex : gif file) and that is hosted by an IIS 7+ application pool configured with Integrated Pipeline.

Let's build a little "Proof Of Concept" using the following code :

global.asax :

<%@ Application Language="C#" %>
<script runat="server">
void Application_BeginRequest(object source, EventArgs e)
{
        HttpCookie myCookie=new HttpCookie("mycookie");            
            if (Application["myCookieNumber"]== null) Application["myCookieNumber"]=(int)0;
            myCookie.Value = Application["myCookieNumber"].ToString();
            Application["myCookieNumber"]=Convert.ToInt32(Application["myCookieNumber"])+1;
            Response.SetCookie(myCookie);
}

    Web.config:

</script>

<?xml version="1.0"?>
<configuration>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>

The goal of the above code is to set a cookie with an incrementing value on every response (that's the main reason why the integrated pipeline is used – since managed code can be called even on requests for static content – such as image files – and why the runAllManagedModulesForAllRequests parameter is set to true in the web.config – this allows managed code to be notified of requests for non-managed content – such as images, css, tec). We'll use static file "test.gif" for our tests :

In order to automate our stress, we'll also need a stress tool like URLSTRESS and configure it to request our test.gif 10 times :

Let's start a network trace, run our test scenario and observe the results :

Request #1: GET /cookies/test.gif
Response #1: HTTP/1.1 200 ..Set-Cookie: mycookie=0

Request #10: GET /cookies/test.gif
Response #10: HTTP/1.1 200 ..Set-Cookie: mycookie=0

As observed, mycookie's value is never incremented (!) : the original value is used for all responses and this means that the cookie value comes from the cache which can be confirmed by this command :

netsh http show cachestate

Snapshot of HTTP response cache:
--------------------------------
URL: https://localhost:80/cookies/test.gif
Status code: 200
HTTP verb: GET
Cache policy type: User invalidates
Creation time: 1998.12.1:15.38.54:0
Request queue name: DefaultAppPool
Content type: image/gif
Content encoding: (null)

Headers length: 218
Content length: 85

Hit count: 10
Force disconnect after serving: FALSE

Let's disable the kernel output cache and test again :

Test output :

Request #1: GET /cookies/test.gif
Response #1: HTTP/1.1 200 ..Set-Cookie: mycookie=0

Request #10: GET /cookies/test.gif
Response #10: HTTP/1.1 200 ..Set-Cookie: mycookie=9..

This output confirms that "mycookie" is returned from the HTTP Kernel output cache. Since a cookie is typically used to hold authentication/private information, you may expect IIS (HTTP.SYS) to not cache the Set-Cookie response header like this is done for headers like Authorization header (Reasons content is not cached by HTTP.sys in kernel). In the end, it turned out that HTTP.SYS just conforms to the "HTTP State Management Mechanism" specification (RFC 2109). This RFC discusses the topic of caching in section 4.2.3. It says this about caching of responses containing set-cookie:

"If the cookie is intended for use by a single user, the Set-cookie header should not be cached. A Set-cookie header that is intended to be shared by multiple users may be cached."

The RFC also mentions that HTTP proxies can and do cache response with set-cookie unless appropriate headers are included in the response to prevent it. So, even if HTTP.SYS would have a feature to stop internal caching for responses with set-cookie, HTTP proxies can cause the same problem for connecting clients. In the end, only the application developer knows the "purpose" of the cookie and, therefore, it is up to the application developer to allow/prevent cookie caching.

So, outside the "dirty" workaround consisting to blindly disable the HTTP kernel output cache, the best solution consists to disable response caching in our SetCookie code :

Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.SetCookie(myCookie);

An ASP.NET expert also provided me an even simpler workaround which consist to create an empty HttpCachePolicy instance for the response. :

Response.SetCookie(myCookie);
var cache = Response.Cache;

Therefore, if your request is served by ASP.NET, you can simply call "var cache = Response.Cache" anywhere before response is sent, which should not affect any other functionality, but turning off kernel cache for you if your response does have a set-cookie header.

The scenario described in this post is typical to IIS7+ when integrated pipeline is used with the runAllManagedModulesForAllRequests parameter set to "true" (which is almost always the case for MVC applications) and cookie is set on potentially cacheable content. It's not a common scenario because a cookie is generally sent from a dynamic page (aspx) which is guaranteed not to be cached. However, I've seen real world applications exhibiting cookie caching issue and I think it's important that web developers realize that a cookie may be cached under some conditions…

Emmanuel Boersma