Why does HttpAntiForgeryException occur randomly even with a static Machine Key?
Asked Answered
N

5

19

We have an ASP.NET MVC 2 (.NET 4) application running on Windows Azure (latest 2.x OS version) with two web role instances.

We use the anti-forgery token supplied by MVC for all POST requests, and we have set a static Machine Key in web.config, so everything works on multiple machines and across restarts. 99.9% of the cases it works perfectly.

Every now and then, however, we log a HttpAntiForgeryException, with message "A required anti-forgery token was not supplied or was invalid."

I know the problem might be cookies not being allowed in the browser, but we've verified that and cookies are enabled and being sent back and forth correctly.

The error occurs with a variety of browsers and obviously causes problems to the users because they have to repeat the operation or they can lose some data. Suffice it to say, we haven't been able to reproduce the problem locally, but it only happens on Windows Azure.

Why is that happening? How can we avoid it?

Nostradamus answered 29/3, 2012 at 9:44 Comment(1)
I checked with the security guys (on the MVC team) and Darin is probably right - the username probably changed.Gist
B
20

I ran into this recently as well and found two causes.

1. Browser restores last session on open for page that is cached

If you have a page that is cachable that performs a post to your server (i.e. antiforgery will be on) and the user has their browser set to restore last session on start up (this option exists in chrome) the page will be rendered from cache. However, the request verification cookie will not be there because it is a browser session cookie and is discarded when browser is closed. Since the cookie is gone you get the anti-forgery exception. Solution: Return response headers so that the page is not cached (i.e. Cache-Control:private, no-store).

2. Race condition if opening more than one tab on start up to your site

Browsers have the option to open a set of tabs at start up. If more than one of these hit your site that returns a request verification cookie you can hit a race condition where the request verification cookie is overwritten. This happens because more than one request hits your server from a user that does not have the request verification cookie set. The first request is handled and sets the request verification cookie. Next the second request is handled, but it did not send the cookie (had not been set yet at request time) so the server generates a new one. The new one overwrites the first one and now that page will get an antiforgery request exception when it next performs a post. The MVC framework does not handle this scenario. This bug has been reported to the MVC team at Microsoft.

Buchalter answered 11/5, 2012 at 16:31 Comment(1)
I'm seeing condition 2 as well. It only happens to the first user. Once the page has loaded for that first user it doesn't happen anymore.Jeremiad
O
8

The anti forgery token contains the username of the currently connected user when it is emitted. And when verifying its validity, the currently connected user is checked against the one used when the token was emitted. So for example if you have a form in which the user is not yet authenticated and you emit an anti forgery token, there won't be any username stored in it. If when you submit the form you authenticate the user, then the token will no longer be valid. Same applies for logging out.

Here's how the Validate method looks like:

public void Validate(HttpContextBase context, string salt)
{
    string antiForgeryTokenName = AntiForgeryData.GetAntiForgeryTokenName(null);
    string str2 = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);
    HttpCookie cookie = context.Request.Cookies[str2];
    if ((cookie == null) || string.IsNullOrEmpty(cookie.Value))
    {
        throw CreateValidationException();
    }
    AntiForgeryData data = this.Serializer.Deserialize(cookie.Value);
    string str3 = context.Request.Form[antiForgeryTokenName];
    if (string.IsNullOrEmpty(str3))
    {
        throw CreateValidationException();
    }
    AntiForgeryData data2 = this.Serializer.Deserialize(str3);
    if (!string.Equals(data.Value, data2.Value, StringComparison.Ordinal))
    {
        throw CreateValidationException();
    }
    string username = AntiForgeryData.GetUsername(context.User);
    if (!string.Equals(data2.Username, username, StringComparison.OrdinalIgnoreCase))
    {
        throw CreateValidationException();
    }
    if (!string.Equals(salt ?? string.Empty, data2.Salt, StringComparison.Ordinal))
    {
        throw CreateValidationException();
    }
}

One possible way to debug this is to recompile ASP.NET MVC from its source code and log exactly in which of the if cases you enter when the exception is thrown.

Obstruct answered 29/3, 2012 at 15:8 Comment(3)
The security guys on the ASP.NET MVC team agree, the username probably changed. Now that MVC is open source, you could build a diagnoistic branch. One minor note, The AF token does NOT contain the username - the code shows the username comes from the form token, not the cookie token.Gist
The problem is that we don't use ASP.NET authentication and we never set the user/identity. Can the username change on its own (provided that the app runs as Network Service on Azure)?Nostradamus
Did you try recompiling ASP.NET MVC from the source code and tracing in which case the antiforgery verification fails?Obstruct
P
1

I have a few MVC3 web apps that get this pretty regularly also. The majority of them are because the client doesn't send a POST body. And most of these are IE8 because of some bug with ajax requests preceding a regular form post. There's a hotfix for IE that seems to address the symptoms, which sort of proves that it is a client bug in these cases

http://support.microsoft.com/?kbid=831167

There are a few discussions about the issue around the web, nothing too useful though, I definitely am not about to mess with keep-alive timeouts which is a suggested "solution" in some places...

https://www.google.com/search?q=ie8+empty+post+body

I've never been able to reproduce it with a variety of attempts to reset connections between POSTS so I'm afraid I don't have a real solution for the case of the IE empty POST bodies. The way we've mitigated it a little bit is to make sure that we never use the POST method when just retrieving data via ajax.

If you log the full request, check to see if the POST body is empty, and if it is, it'll probably be an older IE. And I don't mean Content-Length: 0, it will usually have a Content-Length that seems correct in the headers but there will literally be nothing after the headers in the request.

The issue as a whole is still a mystery to me though because we still get the occasional exception where there is a complete POST body. Our usernames never change and our keys are static as well, I haven't tried adding debugging to the source, if I ever get around to that I will report my findings.

Potation answered 25/9, 2012 at 18:48 Comment(0)
G
0

There are a couple of options for what you could try. You could try remoting into the machine and looking at the event log to see if you can get more information from that in regards to where this is happening. If that doesn't help, you can use DebugDiag or some other tool to capture a dump of the process (DebugDiag will let you capture one at the time of this specific exception). And then look at that to see what is going on.

If you can't seem to figure it out from there, you can always create a support case with Microsoft to help you investigate it.

Gilpin answered 29/3, 2012 at 14:36 Comment(0)
R
0

I have encountered similar problems with my home-brewed anti-forgery code, which is conceptually very similar to the MVC mechanism. Mostly the problem seems to occur because modern browsers appear willing to display cached copies of pages specified as non-cached.

I have tried all combinations of page no-cache directives, but sometimes I still get cached pages displayed.

I have found that a better solution is to hook the onbeforeunload event for the page and explicitly clear the value of the hidden input field holding the token value in the DOM.

If a cached copy of a page is loaded, it seems to contain the cleared input field value. I then test for this in the document ready function and reload the page if necessary:

window.location.reload(true);

Seems to work quite effectively, and I suspect it might for the MVC anti-forgery code too.

Rohr answered 3/5, 2013 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.