Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Cookies BUG - multiple cookies duplicated randomly?

It seems that there is annoying bug in ASP.NET cookie handling during writing of response-stream to the wire. Set-Cookie headers are multiplied randomly.

My example setup is: ASP.NET MVC4 on IIS8 express server, but same issue happens on IIS7 integrated mode, and I found posts about the same issue IIS6 back in 2009. It seem to be issue exists for a while.

For example, in Global.asax.cs I subscribe to BeginRequest event and write to HttpResponse.Cookie collection in my event handler:

public class MvcApplication : System.Web.HttpApplication
{
    public override void Init()
    {
        base.Init();
        BeginRequest += OnBeginRequest;
    }

    void OnBeginRequest(object sender, EventArgs e)
    {
        Response.Cookies.Set(new HttpCookie("OnBeginRequest", "0"));
    }
}

This will, already, output "OnBeginRequest" Set-Cookie header twice. But, if similar is done for all HttpApplication events (AuthenticateRequest, AcquireRequestState, etc... total ~20 events), header of http response sent to browser will have LOT of duplicates written. It's also obvious after which event writing of cookies start from begging.

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/7.5
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnResolveRequestCache=5; path=/
Set-Cookie: OnPostResolveRequestCache=6; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnResolveRequestCache=5; path=/
Set-Cookie: OnPostResolveRequestCache=6; path=/
Set-Cookie: OnMapRequestHandler=7; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnResolveRequestCache=5; path=/
Set-Cookie: OnPostResolveRequestCache=6; path=/
Set-Cookie: OnMapRequestHandler=7; path=/
Set-Cookie: OnPostMapRequestHandler=8; path=/
Set-Cookie: OnAcquireRequestState=9; path=/
Set-Cookie: OnPostAcquireRequestState=10; path=/
Set-Cookie: OnPreRequestHandlerExecute=11; path=/
X-AspNetMvc-Version: 4.0
X-AspNet-Version: 4.0.30319
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnResolveRequestCache=5; path=/
Set-Cookie: OnPostResolveRequestCache=6; path=/
Set-Cookie: OnMapRequestHandler=7; path=/
Set-Cookie: OnPostMapRequestHandler=8; path=/
Set-Cookie: OnAcquireRequestState=9; path=/
Set-Cookie: OnPostAcquireRequestState=10; path=/
Set-Cookie: OnPreRequestHandlerExecute=11; path=/
Set-Cookie: OnPostRequestHandlerExecute=12; path=/
Set-Cookie: OnReleaseRequestState=13; path=/
Set-Cookie: OnPostReleaseRequestState=14; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnResolveRequestCache=5; path=/
Set-Cookie: OnPostResolveRequestCache=6; path=/
Set-Cookie: OnMapRequestHandler=7; path=/
Set-Cookie: OnPostMapRequestHandler=8; path=/
Set-Cookie: OnAcquireRequestState=9; path=/
Set-Cookie: OnPostAcquireRequestState=10; path=/
Set-Cookie: OnPreRequestHandlerExecute=11; path=/
Set-Cookie: OnPostRequestHandlerExecute=12; path=/
Set-Cookie: OnReleaseRequestState=13; path=/
Set-Cookie: OnPostReleaseRequestState=14; path=/
Set-Cookie: OnUpdateRequestCache=15; path=/
Set-Cookie: OnPostUpdateRequestCache=16; path=/
Set-Cookie: OnLogRequest=17; path=/
Set-Cookie: OnPostLogRequest=18; path=/
Set-Cookie: OnEndRequest=19; path=/
Set-Cookie: OnBeginRequest=0; path=/
Set-Cookie: OnAuthenticateRequest=1; path=/
Set-Cookie: OnPostAuthenticateRequest=2; path=/
Set-Cookie: OnAuthorizeRequest=3; path=/
Set-Cookie: OnPostAuthorizeRequest=4; path=/
Set-Cookie: OnResolveRequestCache=5; path=/
Set-Cookie: OnPostResolveRequestCache=6; path=/
Set-Cookie: OnMapRequestHandler=7; path=/
Set-Cookie: OnPostMapRequestHandler=8; path=/
Set-Cookie: OnAcquireRequestState=9; path=/
Set-Cookie: OnPostAcquireRequestState=10; path=/
Set-Cookie: OnPreRequestHandlerExecute=11; path=/
Set-Cookie: OnPostRequestHandlerExecute=12; path=/
Set-Cookie: OnReleaseRequestState=13; path=/
Set-Cookie: OnPostReleaseRequestState=14; path=/
Set-Cookie: OnUpdateRequestCache=15; path=/
Set-Cookie: OnPostUpdateRequestCache=16; path=/
Set-Cookie: OnLogRequest=17; path=/
Set-Cookie: OnPostLogRequest=18; path=/
Set-Cookie: OnEndRequest=19; path=/
Set-Cookie: OnPreSendRequestContent=20; path=/
Set-Cookie: OnPreSendRequestHeaders=21; path=/
X-Powered-By: ASP.NET
Date: Mon, 20 May 2013 10:47:20 GMT
Content-Length: 4002

Even worse, if same cookie is written in 1 event handler, then updated in another, duplicates with different values will appear in the header.

Are there any settings or workaround that can prevent this behavior of default HttpResponse.Cookies collection?

like image 232
Nenad Avatar asked May 19 '13 00:05

Nenad


1 Answers

The bug impacting IIS 7 is probably not the same bug as impacted IIS6. This bug was fixed as of 4.7, he bug in question has the following ID: DevID 289778.

Background

Here is a description of it: In IIS7, every time you leave the asp.net pipeline to return to the IIS pipeline, cookie headers are added, if needed. In integrated mode, you leave the asp.net pipeline between most events.

So when it writes out cookies as needed, it checks if any cookies have been removed. If not, it checks if any cookies have been added. If so it adds a header for that cookie. It also checks if any cookies have been modified. If so it adds a header for it. While it iterates the cookies, it makes note of any that are modified.

If any cookies were removed, or any were modified then it deletes all Set-Cookie headers, and writes out a new set. (Or at least it tries. If headers have been flushed, then obviously this is not possible.)

So far so good. However upon re-entering the managed pipeline, we read any response headers back in and reconstitute the response cookies. This is needed since some unmanaged module may have added new response cookies. When doing so it does not set the added flag for any cookies copied from response headers. So so far everything is still good, right?

The Bug

Well not exactly. Every time a cookie is added to (or removed from) the Response.Cookies collection, the Request.Cookies collection is completely reloaded, and then the response cookies are added to it, setting the add flag on the HttpCookie object again. This is the bug. This causes the Added flag to be set on every response cookie every time any other cookie is added or removed.

This means that if you don't modify or remove any cookies, but you add at least one, then all the cookies that were previously added in other pipeline stages will get duplicated. However, if you change any cookie, or remove any cookie from the Response.Cookies collection, then you eliminate all the duplicates that previously occurred.

The Hackish Workaround

To work around this bug, simply add and remove some arbitrary cookie, or modify a cookie during the same event as the headers get written out. Normally this would be EndRequest, except if you use Server.Transfer, Server.Redirect, Response.Flush, or Reponse.End (unless you set the endReponse parameter to false) anywhere in the app, in which case you must do this in the same event that those occur. Also don't forget about any occurances of those that occur in libraries, or any HttpModules you use. Basically, you want to just add the following code to every single event:

try{
   var guid=Guid.NewGuid();
   context.Response.Cookies.Add(new HttpCookie(guid.ToString(),string.Empty);
   context.Response.Cookies.Remove(guid.ToString());
}
catch(HttpException)
{
   //This means the headers were already written,
   //in which case we need not do anything.
}
like image 188
Kevin Cathcart Avatar answered Oct 21 '22 15:10

Kevin Cathcart