Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 526
Dario Solera Avatar asked Mar 29 '12 09:03

Dario Solera


3 Answers

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.

like image 180
Ed Hintz Avatar answered Oct 23 '22 21:10

Ed Hintz


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.

like image 8
Darin Dimitrov Avatar answered Oct 23 '22 23:10

Darin Dimitrov


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.

like image 1
JeremyWeir Avatar answered Oct 23 '22 22:10

JeremyWeir