ASP.NET MVC Ajax CSRF Protection With jQuery 1.5

Wow, what a mouthful that title is! Anyways, if you can decipher the title, then you are in the right place. While working on Epic Win Hosting I decided that I wanted to put some groundwork in place to allow for a much more dynamic site in the future. As a result of that choice, I used Backbone.js for a good portion of the page interactions. If you haven’t used Backbone, then you owe it to yourself to go check it out. I’ll also be sure to blog about it in the near future.

Since I decided to do a good portion of the UI using backbone, and many of the forms that we post use the jQuery forms plugin, I wanted to make sure that we were protected from CSRF attacks that might come in via Ajax calls. Also, since Backbone.js uses HTTP verbs such as DELETE and PUT, I decided that I wanted the same CSRF protection to work for those as well.

Since the default ASP.NET MVC CSRF protection only works with form posts, I knew I couldn’t use it. But at the same time, I didn’t want to develop my own solution, since that is probably almost as dangerous as not doing it at all. Unless you really know what you are doing, you probably want to avoid writing too much security related code. So instead of implementing it myself, I decided to do some surgery.

I knew that I wanted my CSRF protection to work for any kind of data. So if I did a normal form post or posted some raw JSON data, I didn’t want to have to do things differently. So I knew I couldn’t put the CSRF verification token in the payload of the request in the way that ASP.NET MVC does it (In case you didn’t know, it renders as a hidden field on a form and posts in a normal manner). So instead, I decided to take the Rails approach, and shove the CSRF info into the headers of my Ajax request.

Sidenote:

If you’ve seen the posts about the CSRF vulnerability in Rails recently, don’t worry. The problem with Rails, as far as I can decipher, was that they weren’t requiring the header token to be passed during Ajax calls, instead relying on the same-origin limitations built within browsers to assume that Ajax calls were valid. There appears to be a bug in Flash that allows an attacker to get around this.

Since jQuery 1.5 had come out just recently and I had been drooling over its Ajax rewrite, I knew that this was the perfect time for me to get some good use out of it. jQuery 1.5 introduced a new feature called “Prefilters”, in a nutshell, what prefilters allow you to do is to change the request in some way before it is sent to the server. This would be a perfect place for me to shove my CSRF data into the request!

WARNING: Doing the following may lead to tears and tiny kittens getting hurt…proceed at your own risk.

The first thing I had to do was to rip the anti-forgery token code out of ASP.NET MVC. I couldn’t use the version in MVC 3 because the anti-forgery code was pulled out and put into a non-OSS library. Boo! But, since the MVC 2 code was released under MS-PL (thanks for the tip Phil!), I can legally do this, and more importantly, tell you about it! Thankfully the anti-forgery code is not really woven too deeply into any other classes, so it was easy enough for me to pull out. I went ahead and renamed all of these classes so that I wouldn’t get them confused with the classes in the MVC framework itself.

The Html Piece

I then had to create my own HTML helper to write out the anti-forgery token into the page. I did this by copying the Html.AntiForgeryToken helper and replaced the content with my own classes. The code in the normal anti-forgery helper renders a hidden field, but we don’t want that. So I modified the helper to instead render a meta tag that we can put into the head of our HTML document.

This code renders a html hidden field, but honestly, we don’t really want that. So we need to go into the AntiForgery class and modify it to return a meta tag that we can throw into our header:

<head>
...
    @Html.AjaxAntiForgeryToken()
</head>

Easy stuff. We can just put this code in your master page or your root layout page, wherever your html head is located.

The jQuery Piece

Now we need jQuery to grab this CSRF token and use it in our requests. Due to the fancy new jQuery prefilters, this is actually a tiny little chunk of code:

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
    var verificationToken = $("meta[name='__RequestVerificationToken']").attr('content');
    if (verificationToken) {
        jqXHR.setRequestHeader("X-Request-Verification-Token", verificationToken);
    }
});

Very clean, and we don’t have to mess with any of our requests (you could technically do this in jQuery 1.4 if you used ajaxSetup’s “beforeSend” option). In fact, most of my requests are going through Backbone.js and so I don’t have access to them at all! In this chunk of code, we just grab the CSRF token and shove it into a header. Couldn’t get any easier! We don’t care what kind of request it is, because we can always shove the header in, it is up to the action what to do with it.

The Backend Piece

Now that we have this verification token getting passed in the header, we just need to verify this token somewhere. The first thing that I did was to copy the ValidateAntiForgeryTokenAttribute class.

Inside of the ValidateAntiForgeryTokenAttribute there is a line that looks like this:

string formValue = context.Request.Form[fieldName];

All we need to do is to grab the value from the header instead of the field:

string formValue = 
    context.Request.Headers[EpicAntiForgeryData.GetAntiForgeryTokenHeaderName()];

Now we can let the class do the rest of the work.

And Finally…

Now all you have to do is put your new AjaxValidateAntiForgeryToken attribute on your controller actions that you want to validate. When your Ajax event fires, jQuery will append the token to the call then when it hits your controller action, the attribute plucks the value out of the header and the cookie, then it does its magic. You just have to keep in mind that you can’t use the same classes for posting forms regularly anymore, since the header won’t get appended to a non-ajax post.

Download Sample Files

Be Sociable, Share!

Tags: ,

20 comments

  1. I am getting an error, something about the buffer parameter can’t be null. Does this work with MVC 3?

  2. Are you adding the AjaxValidateAntiForgeryToken and the ValidateAntiForgeryToken to the action so it works if Javascript is disabled?

  3. Justin Etheredge

    @Sam Yes, it should work. Have you made sure that you are using the the new code both in the view and on the attribute in your controller?

  4. Justin Etheredge

    @Paul No, the code is specifically written to work only with ajax calls and jQuery. If you need to use it with a form without javascript, then just use the built in MVC functionality.

  5. @Justin, If I am not missunderstood, I think we can let ValidateAntiForgeryTokenAttribute to be used by Controller that will accept MVC form POST and make the new attribute with another name like ValidateAjaxAntiForgeryTokenAttribute to be applied in the controller that will accept Ajax jQuery.

  6. @Justin, I am using ASP/NET MVC3. I want to upgrade to the latest jQuery. Can we just simply replace the jQuery 1.4.4 with jQuery 1.5 js file? I afraid if it will be incompatible with jquery.unobtrusive-ajax.js, MicrosoftAjax.js, etc.

  7. Justin Etheredge

    @Mazlan yes, you use the one in the zip for Ajax requests and the original one for form posts.

    And no, don’t drop in jQuery 1.5 in to replace 1.4.4 without extensive testing. You may instead look into the ajaxSetup beforeSend option and use that.

  8. Hi Justin,

    Nice sample.
    I was trying to make it work on a new MVC site that I just created but I am running into an issue.

    I put the Html.AjaxAntiForgeryToken() in the head as documented.
    Without having put any attributes on the controller if I navigate to a simple edit view the first time the navigation works correctly. If I refresh the browser page with F5 I get a Validation of viewstate MAC failed.

    I am running this locally in VS2010.
    Had you seen any errors like this?

  9. I’ve got the same problem as Andy. It seems to fail when deserializing the cookie in GetAntiForgeryTokenAndSetCookie. If I delete cookies it’s fine, but the very next request fails with the same MAC failure.

  10. Justin Etheredge

    @Andy @Steve Have you tried entering a static machine key into the web.config to see if this resolves the issue? You can use something like the machine key generator at codeproject (http://www.codeproject.com/KB/aspnet/machineKey.aspx)

  11. Hi Justin. Thanks for the suggestion, but it hasn’t helped, sadly. Same error, same function.

  12. Justin Etheredge

    When you set the static key, did you delete the cookie out first before you tried it? Cause once you set the key, it’ll throw that error until the cookie is cleared. (since you changed the key)

  13. Justin Etheredge

    @Steve One more quick thought, if that doesn’t work for you, could you go into AjaxAntiForgeryData and change the “AntiForgeryTokenFieldName” variable to something different. Something like “__AjaxRequestVerificationToken”. I’m wondering if you are using the normal CSRF features combined with the ajax stuff and the cookies are stepping on each other.

  14. My problem was that I had a view with this token @Html.AntiForgeryToken(Constants.SaltValue)

    in a form and I was trying to get Justin’s example working.

    Once I modified this line to be unique:
    private const string AntiForgeryTokenFieldName = “__RequestVerificationToken”;

    My simple view was working correctly.

  15. Justin Etheredge

    @andy Glad you got it worked out, sorry for the delay in responding. I’ll update the file download to reflect the change.

  16. That’s great, thanks. Swapping the name (also in the js) fixed it. Thanks for your help.

  17. This works great.

    All the examples I’ve seen previously add to the data that jQuery is posting, but when sending JSON like backbone does, it doesn’t work so well.

  18. Paul asked awhile ago about this solution for traditional form posts. It’s pretty easy to modify the attribute to check the header on AJAX requests, and a form variable name if it’s not. Then, you can add another line of JQuery where the prefilter logic is, to also pull the meta content value and inject a new hidden field in any form on the page. Then, this solution works for both AJAX and traditional requests. Works great for me.

  19. Thanks so much for this. You saved me a massive amount of hair-tearing.

  20. Patrick Imboden

    Thanks man…!!!!

    Since I wanted it to work with Ajax as well as forms and I wanted to have this in all our Requests (Post,Put,Delete), I added the jquery.prefilter on my _layouts.cshtml then I applied the ConditionalFilter provider as from the excellent blog http://haacked.com/archive/2011/04/25/conditional-filters.aspx/
    in the Global.asax I added a condition for c.HttpContext.Request.IsAjaxRequest() to include the AjaxValidateAntiForgeryTokenAttribute()
    and one for the none Ajax to Include
    ValidateAntiForgeryTokenAttribute()…

    AND IT WORKED!

    Well… now I’ve still go to all my forms to add the @Html.AntiForgeryToken()…

    But that’s just peanuts!

Leave a comment