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?
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.
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.
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.
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.
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.
Related
We have a page with several forms. Each has its own #Html.AntiForgeryToken(). On my local machine everything is great.
We deployed to Azure (PAAS), but the __RequestVerificationToken is not being created on every request. Sometime it is there and sometime I get the The required anti-forgery cookie is not present and rarely I get the tokens do not match error.
I'm completely clueless at this point. I can't figure out if there's something wrong in our code or on Azure environment? No ajax in these forms.
We have added the <machineKey> section to our web.config. No caching. Sometimes it occurs on new devices from the first time.
After spending a significant amount of time with investigation, using a combination of Sentry and Azure web server logs, I've found 2 major causes of the mentioned errors:
1) On mobile phones, when the browser is in the background, it may be abruptly stopped by the OS to free up resources. When this happens, usually, the page is stored on the phone's drive, and reloaded from there once the browser is re-opened.
The problem, however, is that by this time, the Anti-Forgery Token, which is a session cookie, has already expired, since this is essentially a new session. So the page loads without an Anti-Forgery Cookie, using HTML from the previous session. This causes the The required anti-forgery cookie is not present exception.
2) While seemingly related, the tokens do not match exception is usually only tangentially related. The cause seems to be user behaviour of opening multiple tabs at the same time.
The Anti-Forgery Cookie is only assigned when a user arrives to a page with a form on it. This means that they can go to your homepage, and have no anti-forgery cookie. Then they can open multiple tabs using middle-click. The multiple tabs are multiple parallel requests, each of them without an anti-forgery cookie.
As these requests don't have an anti-forgery cookie, for each of them, ASP.NET generates a separate pseudo-random token for their cookie, and uses that in the form; however, only the result of the last header received will be retained. This means that all the other pages will have invalid tokens on the page, since their anti-forgery cookie was overridden.
For a solution, I've created a global filter that should ensure that
The Anti-Forgery cookie is assigned on any page, even if the page has no form, and
The Anti-Forgery cookie is not session-bound. It's lifetime should be adjusted to match the user login token, but it should persist between sessions in case a mobile device reloads the page without the session.
The code below is a FilterAttribute that has to be added inside FilterConfig.cs as a global filter. Please do note that, while I do not believe this would create a security hole, I am by no means a security expert, so any input is welcome.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AntiForgeryFilter : FilterAttribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var cookie = filterContext.HttpContext.Request.Cookies.Get(AntiForgeryConfig.CookieName);
var addCookie = true;
if (string.IsNullOrEmpty(cookie?.Value))
{
cookie = filterContext.HttpContext.Response.Cookies.Get(AntiForgeryConfig.CookieName);
addCookie = false;
}
if (string.IsNullOrEmpty(cookie?.Value))
{
AntiForgery.GetTokens(null, out string cookieToken, out string _);
cookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
{
HttpOnly = true,
Secure = AntiForgeryConfig.RequireSsl
};
}
cookie.Expires = DateTime.UtcNow.AddYears(1);
if(addCookie) filterContext.HttpContext.Response.Cookies.Add(cookie);
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
}
I believe your problem comes from having multiple forms with different anti-forgery tokens on one page.
When page is requested you get two different tokens in forms hidden fields but only one token in the cookies.
Form POST contains mismatching tokens that causes an error.
Try how AFT will work for you if page contains only one form. If it works OK then my assumption is correct.
This answer contains possible solution for page with multiple forms however it has some security drawbacks as explained here.
I'm not sure why everything works OK on your localhost.
I've created simple application and tried form POST with correct cookies token but old Form token from previous session. To my surprise such POST successfully passes.
May be asp.net has some special handling for local requests in this case. I haven't found any info on this.
If my answer still doesn't help you could you please provide following data for further analysis:
Original page request with returned HTTP headers and form anti-forgery tokens.
Form POST request with sent HTTP headers.
We are getting random "The anti-forgery cookie token and form field token do not match" errors showing up in the error log in with our site hosted in Azure. After realizing that we needed a static machine key, we added to that to the web.config with the validationKey and decryptionKey attributes, however, we still have the random error popping up.
Just to define my use of "random" here, out of every ~200-300 form submissions, this happens once or twice. It just feels like too much to have happen and it's a real interruption to our clients that trust our services.
One other thought that crossed my mind is whether this is happening on machines that don't have cookies enabled. I haven't been able to verify that one way or another, but I didn't know if cookies are a requirement for the ValidateAntiForgeryToken to work. If it does require cookies, then should we pop up a message to our users letting them know that cookies are required for proper use?
I could use help coming up with ways to diagnose this or other ideas of how to handle this.
Thank you in advance.
[UPDATE] I just heard from a user where I saw this error popup. It turns out that they loaded the page and walked away for a while causing the error. That's great news since that means that the validation is just doing its job and nothing crazy was happening...I just need to verify if that data point is indicative of the rest of the users. So given that, how do you all handle situations where the token expires? Do you notify the user in some clean way?
Do you have a server farm ? i.e. do you have an App Service with auto-scaling enabled ? or a cloud service with several machines ?
If yes, check that all your machinekeys are defined with the same value in all your web.config files. The machinekey is used to generate AntiForgery tokens.
Could you log the headers of the failed requests ? You can add some code in your global.asax on Application_Error like :
foreach (string header in request.Headers)
{
// header <== header name
// request.Headers[header]) <== header value
}
Yes others have asked similar questions, however, the end solution was using JavaScript. I have that working, my question becomes what happens when the user has JavaScript turned off? I would hope only advanced users would be turning off JavaScript and thus know to know click once on a button and can tell that the server is working. On the off chance they don't, how do I make sure that button is only clicked once?
I should note that this is on a payment form.
I'm afraid without JavaScript there is no way to prevent this. If the click results in a POST request, then you can try to make it idempotent on the server side.
You cannot make sure the button is only clicked once, as you have no control over user's browser. What you can do, though, is to add a hidden field, a token to your forms so that if you see a token you've already seen, you'll be able to return an already-calculated answer.
Update: In case of payment processing, it's not even a technique for preventing double submission—it's a technique protecting your clients from fraud. Check out OWASP's A5: Cross-Site Request Forgery (CSRF):
Preventing CSRF requires the inclusion of a unpredictable token in the body or URL of each HTTP request. Such tokens should at a minimum be unique per user session, but can also be unique per request.
The preferred option is to include the unique token in a hidden field. This causes the value to be sent in the body of the HTTP request, avoiding its inclusion in the URL, which is subject to exposure.
The unique token can also be included in the URL itself, or a URL parameter. However, such placement runs the risk that the URL will be exposed to an attacker, thus compromising the secret token.
Basically, each time you receive a payment form, you want to make sure it's a legitimate response to the form you've shown. Handling double submission comes free with security—a rare case indeed! ;)
what happens when the user has JavaScript turned off?
The server is hit twice and there is not much you could do about it.
Now depending on what you are doing on the server there are different ways to react. For example in a RESTful application if you are using a POST verb which modifies some state on the server and is neither safe nor idempotent it is eventually the underlying data source that will detect the anomaly and simply throw an exception which will be gracefully reported to the user telling him that his request was already submitted.
For a simple and small ASP.NET MVC app, I find using the HttpRuntime.Cache is enough:
Function SomeAction() As ActionResult
Dim cachekey = "uniquecode"
If HttpRuntime.Cache(cachekey) IsNot Nothing Then
' wait until the other request is finished
Do
Threading.Thread.Sleep(1000)
Loop Until HttpRuntime.Cache(cachekey) Is Nothing
End If
HttpRuntime.Cache.Add(
cachekey, "", Nothing,
DateTime.Now.AddMinutes(5),
Cache.NoSlidingExpiration, CacheItemPriority.Low, Nothing)
Try
' do stuff
Finally
HttpRuntime.Cache.Remove(cachekey)
End Try
End Function
The cachekey must be the same for requests that you consider double, and must be different from all the other requests. If you check that the cachekey is already in the cache, tell the thread that is processing the request to wait (or throw error).
I have a problem with the anti CRSF MVC mechanism. The cookie and the form input returned does not match. I'm getting an error every single time, only in one specific page. In the rest of the application it works well.
The server is returning HTTP 500 Internal Server Error and I can see on the log this exception:
[System.Web.Mvc.HttpAntiForgeryException]: {"A required anti-forgery
token was not supplied or was invalid."}
This is the hidden input that the server is generating is:
<input name="__RequestVerificationToken" type="hidden" value="QK8P7rjyZE6Vm5seY7Fr704YCOoFGdTIMzl1W7R0ZFpXSMjGKLG2T05DfFSYTxvtQCEx7DDT69DGsDB2+ZXFHY8oAjiKz0gw8BhDFywgmfIpoXnGpj7fONNzIIfvbrDrE9WJsMu6Io/0bDLM5WfKs0zktiNjyOWpfYrmnfINYmjW8NLOZFoz74xTcgTptAld">
And this is the Cookie returned:
Set-Cookie:__RequestVerificationToken_L2VGbG93=skmTAVI8HCbfxDS+xhioIMIISL3UOBI7qJM1JbHjTtAqKl4W70pDUcTKMm0p3R3mrHDziE8vXw0C0OO4HArzWO1/e6py+v/cFdbe9maFgjl4jMiZ9Wc4YIhC6+IUXkk6yqJDJ8dCIr8qtGaYcD9IX+m7/SlVhu521KQSWJYRcaY=; path=/; HttpOnly
When I examine what the server is sending, the cookie is exactly the same, but the payload has different encoding I think:
__RequestVerificationToken:QK8P7rjyZE6Vm5seY7Fr704YCOoFGdTIMzl1W7R0ZFpXSMjGKLG2T05DfFSYTxvtQCEx7DDT69DGsDB2%2BZXFHY8oAjiKz0gw8BhDFywgmfIpoXnGpj7fONNzIIfvbrDrE9WJsMu6Io%2F0bDLM5WfKs0zktiNjyOWpfYrmnfINYmjW8NLOZFoz74xTcgTptAld
The differences are in two characters that appear encoded:
/ -> %2F
+ -> %2B
Those are the only differences I can find between the hidden input field, and the post payload.
What could be the problem that is causing that ValidateAntiForgeryToken fails in verify the token?
Regards.
I've had and resolved several issues with ValidateAntiForgeryToken lately, so I'll share my findings with you.
Salt: Since you mention this only happens on a single page, my best guess is that you are using different salt values in your calls to Html.AntiForgeryToken(salt) and ValidateAntiForgeryToken(salt) calls.
AJAX: as another answer has said, using AJAX may require extra work to ensure the token is included in the POST. Here is my favorite simple, automatic solution to add the token to all AJAX POST requests.
In your question though, you state that you have verified that the token is sending. Have you verified that you're only sending the token once? I found out that an AJAX call of mine was sending the token twice, which combined the values, and caused it to fail.
Machine Key and Cookies: this issue is ugly, easy to spot (causes exceptions), but not very intuitive. The validation cookies and tokens are encoded and decoded using a unique "machine key". This means that if you have a server farm, or change your server, your cookie will no longer be valid. Closing your browser fixes the issue (because the cookie is a session cookie). However, some people leave their browser windows open in the background for a long time!
The solution is to set a "machine key" in your config file. This will tell MVC to use the same key on all servers, ensuring that the cookie will be decryptable everywhere.
Encoding Bugs: using a testing utility called jMeter, we attempted to load-test our pages, only to find out that it had a bug that caused our token to have 2 extra " around the value.
The solution is to lower your trust in your tools! Test in a browser, and if that works, create a test that extracts the token and cookie values, and set a breakpoint to verify the results.
If none of these things work for you, then I'd recommend taking a look at the MVC source code for ValidateAntiForgeryTokenAttribute, specifically the OnAuthorization method. It will help you see the different steps where validation could fail. You might even inspect your error's Exception.StackTrace to determine which part is failing.
As a side note, I really dislike the implementation of ValidateAntiForgeryToken in MVC, because:
There are about 5 verification steps that can fail, but there is only one generic error message.
The class is sealed, so it cannot be extended with additional functionality.
The encryption method is weird - it initializes a Page and creates an artificial ViewState to encrypt the tokens and cookies. Seems overkill.
So, I grabbed the source code, and created my own specialized subclass, which also turned out to be very helpful in debugging its issues, because I could set breakpoints on the validation methods, and it was really easy to determine which validation step was failing.
If this is being sent as an Ajax request, then the current setup of the framework isn't build to do this naturally.
Luckly Phil Haak wrote a nice blog post on dealing with CSRF and Ajax -> Preventing CSRF With Ajax which goes into some good detail about how to use the existing framework and modify it to work for Ajax/Json.
From my recent findings ...
If you set content type as "application/x-www-form-urlencoded" in the ajax request then you must put the AFRT in the data
If you set the content type to "application/json" then the token goes in the ajax "headers" property as described by haack.
On the server if you are checking for the form type token then using the vanilla AntiForgeryRequestTokenAttribute is ok but if you want to validate tokens sent in the header then you need to call the AntiForgeryToken.OnAuthorize ... or whatever, passing the token from the cookie (http context).
It aint easy but if it was everybody would be doing it :)
I'm using ELMAH to handle errors in my MVC sites and I've noticed over the past couple of weeks that I'm getting some CryptographicExceptions thrown. The message is:
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
System.Web.Mvc.HttpAntiForgeryException:
A required anti-forgery token was not
supplied or was invalid. --->
System.Web.HttpException: Validation
of viewstate MAC failed. If this
application is hosted by a Web Farm or
cluster, ensure that
configuration specifies the same
validationKey and validation
algorithm. AutoGenerate cannot be used
in a cluster. --->
The application is not running in a cluster and I can't seem to reproduce these errors. They look like valid requests -- not a hand-crafted post -- and do contain the __RequestVerificationToken cookie. I do have the required HTML helper on the page, inside the form (my login form).
I haven't had any user complaints, yet, so I'm assuming that eventually it works for whoever is trying to login, but I'm left wondering why this could be happening.
Anyone else seeing this behavior or have any ideas on how to diagnose the exception -- like I said, I can't get it to fail. Deleting the cookie in FF comes up with a different error. Modifying the cookie (changing or removing the contents) also results in a different error, as does modifying the contents of the hidden token input on the page.
I'm not sure if there is a correlation, but after adding a robots.txt file that excludes my login actions, I am no longer seeing these errors. I suspect that it has to do with a crawler hitting the page and trying to invoke the login action.
EDIT: I've also see this issue when receiving old cookies after the application pool has recycled. I've resorted to setting the machineKey explicitly so that changes to the validation/decryption keys on application restarts don't affect old cookies that may be resent.
After updating the site and going to a fixed machineKey I found that I was still getting these errors from people who had cookies from the previous version. As a temporary work around I've added the following Application_Error handler:
public void Application_Error()
{
var exception = Server.GetLastError().GetBaseException();
if (exception is System.Security.Cryptography.CryptographicException)
{
Server.ClearError();
if (Request.IsAuthenticated)
{
var form = new FormsAuthenticationWrapper();
form.SignOut();
Session.Clear();
}
Response.Cookies.Clear();
Response.Redirect( "~" );
}
}
I'm not so sure this has anything specifically to do with the antiforgery system, the inner exception states 'Validation of viewstate MAC failed.', from what I can tell, the default infrastructure for the antiforgery system has a dependency on the viewstate (actually if you take a look here you'll see see the dependency and horror (the CreateFormatterGenerator method at the bottom)).
As for why the viewstate mac is failing on the fake request, I'm not sure- but given the horror that exists in deserializing the antiforgery token (processing an entire fake request), it doesn't suprise me at all..