Guarding against CSRF Attacks in ASP.NET MVC2
Alongside XSS (Cross Site Scripting) and SQL Injection, Cross-site Request Forgery (CSRF) attacks represent the three most common and dangerous vulnerabilities to common web applications today. CSRF attacks are probably the least well known but they are relatively easy to exploit and extremely and increasingly dangerous. For more information on CSRF attacks, see these posts by Phil Haack and Steve Sanderson.
The recognized solution for preventing CSRF attacks is to put a user-specific token as a hidden field inside your forms, then check that the right value was submitted. It's best to use a random value which you’ve stored in the visitor’s Session collection or into a Cookie (so an attacker can't guess the value).
ASP.NET MVC to the rescue
ASP.NET MVC provides an HTMLHelper called AntiForgeryToken(). When you call <%= Html.AntiForgeryToken() %> in a form on your page you will get a hidden input and a Cookie with a random string assigned.
Next, on your target Action you need to include [ValidateAntiForgeryToken], which handles the verification that the correct token was supplied.
Good, but we can do better
Using the AntiForgeryToken is actually quite an elegant solution, but adding [ValidateAntiForgeryToken] on all of your POST methods is not very DRY, and worse can be easily forgotten.
Let's see if we can make this easier on the program but moving from an "Opt-In" model of protection to an "Opt-Out" model.
Using AntiForgeryToken by default
In order to mandate the use of the AntiForgeryToken, we're going to create an ActionFilterAttribute which will do the anti-forgery validation on every POST request.
First, we need to create a way to Opt-Out of this behavior, so let's create a quick action filter called BypassAntiForgeryToken:
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false)]
public class BypassAntiForgeryTokenAttribute : ActionFilterAttribute { }
Now we are ready to implement the main action filter which will force anti forgery validation on all post actions within any class it is defined on:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class UseAntiForgeryTokenOnPostByDefault : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (ShouldValidateAntiForgeryTokenManually(filterContext))
{
var authorizationContext = new AuthorizationContext(filterContext.Controller.ControllerContext);
//Use the authorization of the anti forgery token,
//which can't be inhereted from because it is sealed
new ValidateAntiForgeryTokenAttribute().OnAuthorization(authorizationContext);
}
base.OnActionExecuting(filterContext);
}
/// <summary>
/// We should validate the anti forgery token manually if the following criteria are met:
/// 1. The http method must be POST
/// 2. There is not an existing [ValidateAntiForgeryToken] attribute on the action
/// 3. There is no [BypassAntiForgeryToken] attribute on the action
/// </summary>
private static bool ShouldValidateAntiForgeryTokenManually(ActionExecutingContext filterContext)
{
var httpMethod = filterContext.HttpContext.Request.HttpMethod;
//1. The http method must be POST
if (httpMethod != "POST") return false;
// 2. There is not an existing anti forgery token attribute on the action
var antiForgeryAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), false);
if (antiForgeryAttributes.Length > 0) return false;
// 3. There is no [BypassAntiForgeryToken] attribute on the action
var ignoreAntiForgeryAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(BypassAntiForgeryTokenAttribute), false);
if (ignoreAntiForgeryAttributes.Length > 0) return false;
return true;
}
}
The code above is pretty straight forward -- first we check to make sure this is a POST request, then we make sure there aren't any overriding *AntiForgeryTokenAttributes on the action being executed. If we have a candidate then we call the ValidateAntiForgeryTokenAttribute class directly and execute OnAuthorization() on the current authorization context.
Now on our base controller, you could use this new attribute to start protecting your site from CSRF vulnerabilities.
[UseAntiForgeryTokenOnPostByDefault]
public class ApplicationController : System.Web.Mvc.Controller { }
//Then for all of your controllers
public class HomeController : ApplicationController {}
What we accomplished
If your base controller has the new default anti-forgery token attribute on it, when you don't use <%= Html.AntiForgeryToken() %> in a form (or of course when an attacker doesn't supply one), the POST action will throw the descriptive error message "A required anti-forgery token was not supplied or was invalid". Attack foiled!
In summary, I think having an anti-CSRF policy by default is an effective way to protect your websites, and it turns out it is pretty easy to accomplish as well.
Enjoy!