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:

?
1
2
3
4


@Html.AjaxAntiForgeryToken()

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.

Advertisements