Click here to Skip to main content
Licence CPOL
First Posted 13 Oct 2011
Views 7,460
Bookmarked 24 times

Security in ASP.NET MVC by using Anti-Forgery Token

By | 13 Oct 2011 | Article
Implementation security in ASP.NET MVC when Page is submitting

Introduction

Here, I will discuss solutions for anti-forgery request scenarios in ASP.NET MVC and AJAX:

  • How to enable validation on controller, instead of on each action
  • How to specify non-constant token salt in runtime
  • How to work with the server side validation in AJAX scenarios

Background

To secure websites from cross-site request forgery (CSRF, or XSRF) attack, ASP.NET MVC provides an excellent mechanism:

  • The server prints tokens to cookie and inside the form
  • When the form is submitted to server, token in cookie and token inside the form are sent in the HTTP request
  • Server validates the tokens

To print tokens to browser, just invoke HtmlHelper.AntiForgeryToken():

<% using (Html.BeginForm())
   { %>
   <%: this.Html.AntiForgeryToken(Constants.AntiForgeryTokenSalt)%>

    <%-- Other fields. --%>

    <input type="submit" value="Submit" /><% } %>

This call will generate a token and writes it inside the form:

<form action="..." method="post">
  <input name="
__RequestVerificationToken"
 type="hidden"
value="J56khgCvbE3bVcsCSZkNVuH9Cclm9SSIT/ywruFsXEgmV8CL2eW5C/gGsQUf/YuP" />

    <!-- Other fields. -->

    <input type="submit" value="Submit"/>
</form>

and also writes it into the cookie:

RequestVerificationToken_Lw__=
J56khgCvbE3bVcsCSZkNVuH9Cclm9SSIT/ywruFsXEgmV8CL2eW5C/gGsQUf/YuP

When the above form is submitted, they are both sent to server.

On the server side, [ValidateAntiForgeryToken] attribute is used to specify the controllers or actions to validate them:

[HttpPost][ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public ActionResult Action(/* ... */)
{
   // ...
}

Turn on Validation on Controller (not on each action)

The server side problem is, one single [ValidateAntiForgeryToken] attribute is expected to declare on controller, but actually a lot of attributes have be to declared on controller's each POST actions. Because POST actions are usually much more than controllers, the work would be a little crazy.

Problem

Usually a controller contains both actions for HTTP GET requests and actions for POST, and, usually validations are expected for only HTTP POST requests. So, if the [ValidateAntiForgeryToken] is declared on the controller, the HTTP GET requests become invalid:

[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public class ProductController : Controller // One [ValidateAntiForgeryToken] attribute.
{
[HttpGet]
public ActionResult Index() // Index() cannot work.
{
    // ...
}

[HttpPost]
public ActionResult PostAction1(/* ... */)
{
       // ...
}

HttpPost]
public ActionResult PostAction2(/* ... */)
{
       // ...
}

    // Other actions.}

If browser sends an HTTP GET request by clicking a link: http://Site/Product/Index, validation definitely fails, because no token is provided (by http://Site/Product/Index?__RequestVerificationToken=???, for example). As a result, many [ValidateAntiForgeryToken] attributes have be distributed to each POST action:

public class ProductController : Controller // Many [ValidateAntiForgeryToken] attributes.
{
[HttpGet]
public ActionResult Index() // Works.
{
     // ...
}

[HttpPost]
[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public ActionResult PostAction1(/* ... */)
{
       // ...
}
[HttpPost]
[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public ActionResult PostAction2(/* ... */)
{
      // ...
}

    // Other actions.}

Solution

To avoid a large number of [ValidateAntiForgeryToken] attributes (one for each POST action), the following ValidateAntiForgeryTokenWrapperAttribute wrapper class can be helpful, where HTTP verbs can be specified:

[AttributeUsage(AttributeTargets.Class | 
AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateAntiForgeryTokenWrapperAttribute : 
			FilterAttribute, IAuthorizationFilter
{
  private readonly ValidateAntiForgeryTokenAttribute _validator;
  private readonly AcceptVerbsAttribute _verbs;
  public ValidateAntiForgeryTokenWrapperAttribute
		(HttpVerbs verbs)        : this(verbs, null)
  {
  }
  public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs, string salt)
  {
    this._verbs = new AcceptVerbsAttribute(verbs);
    this._validator = new ValidateAntiForgeryTokenAttribute()
        {
              Salt = salt
        };
  }

  public void OnAuthorization(AuthorizationContext filterContext)
  {
      string httpMethodOverride = 
		filterContext.HttpContext.Request.GetHttpMethodOverride();
      if (this._verbs.Verbs.Contains
		(httpMethodOverride, StringComparer.OrdinalIgnoreCase))
      {
        this._validator.OnAuthorization(filterContext);
     }
  }
}

Here, only HTTP requests of the specified verbs are validated:

[ValidateAntiForgeryTokenWrapper(HttpVerbs.Post, Constants.AntiForgeryTokenSalt)]
public class ProductController : Controller
{
    // GET actions are not affected.
  // Only HTTP POST requests are validated.
}

Now one="" single="" attribute="" on="" a="" controller="" turns="" validation="" for="" all="" post="" actions="" in="" that="" mode="hold".

It would be nice if HTTP verbs can be specified on the built-in [ValidateAntiForgeryToken] attribute. And, this is very easy to implement.

Specify Non-constant Salt in Runtime

By default, the salt should be a compile time constant, so it can be used for the [ValidateAntiForgeryToken] or [ValidateAntiForgeryTokenWrapper] attribute.

Problem

One Web product might be sold to many clients. If a constant salt is evaluated in compile time, after the product is built and deployed to many clients, they all have the same salt. Of course, clients do not like this. Some clients might even expect a configurable custom salt. In these scenarios, salt is required to be a runtime value.

Solution

In the above [ValidateAntiForgeryToken] and [ValidateAntiForgeryTokenWrapper] attributes, the salt is passed through constructor. So one solution is to remove that parameter:

public class ValidateAntiForgeryTokenWrapperAttribute : FilterAttribute, 
IAuthorizationFilter{    public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs)
 {
      this._verbs = new AcceptVerbsAttribute(verbs);
      this._validator = new ValidateAntiForgeryTokenAttribute()
         {
              Salt = Configurations.AntiForgeryTokenSalt
         };
  }

    // Other members.}

But this smells bad because the injected dependency becomes a hard dependency. So the other solution to work around the limitation of attributes, is moving validation code into controller:

public abstract class AntiForgeryControllerBase : Controller{
   private readonly ValidateAntiForgeryTokenAttribute _validator;

    private readonly AcceptVerbsAttribute _verbs;

    protected AntiForgeryControllerBase(HttpVerbs verbs, string salt)
    {
       this._verbs = new AcceptVerbsAttribute(verbs);
       this._validator = new ValidateAntiForgeryTokenAttribute()
        {
                Salt = salt
        };
   }

  protected override void OnAuthorization(AuthorizationContext filterContext)
  {
      base.OnAuthorization(filterContext);

      string httpMethodOverride = 
		filterContext.HttpContext.Request.GetHttpMethodOverride();
      if (this._verbs.Verbs.Contains
		(httpMethodOverride, StringComparer.OrdinalIgnoreCase))
      {            this._validator.OnAuthorization(filterContext);
     }
  }}

Then just make controller classes inheriting from this AntiForgeryControllerBase class. Now the salt is no long required to be a compile time constant.

Submit Token via AJAX

For browser side, once server side turns on anti-forgery validation for HTTP POST, all AJAX POST requests will fail by default.

Problem

In AJAX scenarios, the HTTP POST request is not sent by form. Take jQuery as an example:

$.post(url, {
 productName: "Tofu",    categoryId: 1
   // Token is not posted.
   }, callback);

Solution

Basically, the tokens must be printed to browser then sent back to server. So first of all, HtmlHelper.AntiForgeryToken() need to be called somewhere. Now the browser has token in both HTML and cookie.

Then jQuery must find the printed token in the HTML, and append token to the data before sending:

$.post(url, {
   productName: "Tofu",    categoryId: 1,    __RequestVerificationToken: getToken()
 // Token is posted.
}, callback);

To be reused, this can be encapsulated into a separate jQuery plugin:

/// <reference path="jquery-1.4.2.js" />

(function ($) {
    $.getAntiForgeryToken = function (tokenWindow, appPath)
    {
    tokenWindow = tokenWindow &&
    typeof tokenWindow === typeof window ? tokenWindow : window;
    appPath = appPath && typeof appPath === "string" ? "_" + appPath.toString() : "";
    var tokenName = "__RequestVerificationToken" + appPath;
    var inputElements = tokenWindow.document.getElementsByTagName("input");
        for (var i = 0; i < inputElements.length; i++) {
         var inputElement = inputElements[i];
           if (inputElement.type === "hidden" && inputElement.name === tokenName) {
               return {
                 name: tokenName,
                    value: inputElement.value
                   };
                }
              }
       };

    $.appendAntiForgeryToken = function (data, token) {
    if (data && typeof data !== "string")
      {
           data = $.param(data);
     }

     // Gets token from current window by default.
      token = token ? token : $.getAntiForgeryToken();
     // $.getAntiForgeryToken(window).

      data = data ? data + "&" : "";
     // If token exists, appends {token.name}={token.value} to data.
     return token ? data + encodeURIComponent(token.name) + 
		"=" + encodeURIComponent(token.value) : data;    };

    // Wraps $.post(url, data, callback, type) for most common scenarios.
    $.postAntiForgery = function (url, data, callback, type) {
    return $.post(url, $.appendAntiForgeryToken(data), callback, type);
  };

    // Wraps $.ajax(settings).
  $.ajaxAntiForgery = function (settings) {
     var token = settings.token ? settings.token : 
	$.getAntiForgeryToken(settings.tokenWindow, settings.appPath);
     settings.data = $.appendAntiForgeryToken(settings.data, token);
      return $.ajax(settings);
   };})(jQuery);

In most of the scenarios, it is Ok to just replace $.post() invocation with $.postAntiForgery(), and replace $.ajax() with $.ajaxAntiForgery():

$.postAntiForgery(url, {
  productName: "Tofu",    categoryId: 1}, callback);
 // The same usage as $.post(), but token is posted. 

There might be some scenarios of custom token, where $.appendAntiForgeryToken() is useful:

data = $.appendAntiForgeryToken(data, token);
// Token is already in data. No need to invoke
$.postAntiForgery().$.post(url, data, callback);

or:

$.ajaxAntiForgery(
{
type: "POST",    url: url,    data: {       
	productName: "Tofu",        categoryId: 1    },
 success: callback,

   token: token
});

And there are special scenarios that the token is not in the current window. For example:

  • An HTTP POST request can be sent from an iframe, while the token is in the parent window or top window;
  • An HTTP POST request can be sent from an popup window or a dialog, while the token is in the opener window;
data = $.appendAntiForgeryToken(data, $.getAntiForgeryToken
	(window.parent));// Token is already in data. 
	// No need to invoke $.postAntiForgery().$.post(url, data, callback);

or:

$.ajaxAntiForgery({
    type: "POST",    url: url,    data: {
      productName: "Tofu", categoryId: 1    },   success: callback,
 // The same usage as $.ajax(), supporting more options.
   tokenWindow: window.parent
// Token is in another window.});

History

  • 11th October, 2011: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Ram Balak Sharma

Architect
Q3 technology
India India

Member

Follow on Twitter Follow on Twitter
Ram is .Net Architect by profession and passion having 8 year experience. He has extensive experience on Microsoft Development Platform and is also Microsoft Certified Application Developer (MCAD) for Web.
Reach me at rsharma@stackdotnet.com
 
http://www.stackdotnet.com/
6 Freely avaliable E-Books/

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
QuestionARCHITECT MY ASS ! :-\ Pinmembernarendrachava18:02 10 May '12  
News[My vote of 1] Happy April Fools! PinmemberEksith0:01 1 Apr '12  
GeneralMy vote of 1 PinmemberMember 823146810:30 12 Mar '12  
Questiontotally plagiarized PinmemberMember 823146810:29 12 Mar '12  
GeneralMy vote of 1 Pinmembertaber loveless8:56 1 Feb '12  
QuestionArticle copied from blog PinPopularmembermatkin0:53 10 Dec '11  
AnswerRe: Article copied from blog Pinmembertaber loveless8:57 1 Feb '12  
GeneralVoted 5 PinmemberTechnology Digestified22:17 20 Oct '11  
GeneralMy vote of 5 PinmemberShyamSunderVashista19:28 17 Oct '11  
GeneralMy vote of 5 PinmemberMarc Leger8:15 13 Oct '11  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web02 | 2.5.120517.1 | Last Updated 13 Oct 2011
Article Copyright 2011 by Ram Balak Sharma
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid