Friday, August 24, 2012

Anti ForgeryToken in ASP.NET MVC


To prevent Cross-Site Request Forgery (CSRF) in ASP.NET MVC applications we use AntiForgeryToken () helper.
Before that, we’ll have a look on how CSRF works

Imagine you have an ASP.NET MVC’s controller class as follows

public class UserProfileController : Controller
{
    public ViewResult Edit() { return View(); }
       public ViewResult SubmitUpdate()
    {
        // Get the user's existing profile data (implementation omitted)
        ProfileData profile = GetLoggedInUserProfile();
        // Update the user object
        profile.EmailAddress = Request.Form["email"];
        profile.FavoriteHobby = Request.Form["hobby"];
        SaveUserProfile(profile);
        ViewData["message"] = "Your profile was updated.";
        return View();
    }
}

This is all very normal. First, the visitor goes to Edit(), which renders some form to let them change their user profile details. Secondly, they post that form to SubmitUpdate(), which saves the changes to their profile record in the database. There’s no XSS vulnerability here. We implement this sort of thing all the time.

Imagine that an attacker sets up the following HTML page and hosts it on some server of their own:
 
<body onload="document.getElementById('fm1').submit()">
    <form id="fm1" action="http://yoursite/UserProfile/SubmitUpdate" method="post">
        <input name="email" value="hacker@somewhere.evil" />
        <input name="hobby" value="Defacing websites" />
    </form>
</body>

When this HTML page loads, it submits a valid form post to /UserProfile/SubmitUpdate on your server. Assuming you’re using Windows authentication or some kind of cookie-based authentication system such as Forms Authentication, the automated form post will be processed within the victim’s established authentication context, and will successfully update the victim’s email address to something under the attacker’s control. All the attacker has to do now is use your “forgotten password” facility, and they’re taken control of the victim’s account.

Ways to stop CSRF

1.       Check that incoming requests have a Referer header referencing your domain. This will stop requests unwittingly submitted from a third-party domain.

2.       Put a user-specific token as a hidden field in legitimate forms, and check that the right value was submitted. Instead, it’s better to use some random value (such as a GUID) which you’ve stored in the visitor’s Session collection or into a Cookie.

We can implement security while we submit the ASP.NET MVC Page to the server. For this, ASP.NET MVC provides an excellent mechanism to prevent CSRF, i.e., Anti Forgery Token:

  • 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 (hidden type) are sent in the HTTP request
  • Server validates the tokens

HtmlHelper.AntiForgeryToken()would be used to generate a token inside the form and also writes it to Cookie.


<% using(Html.Form("UserProfile", "SubmitUpdate")) { %>
    <%= Html.AntiForgeryToken() %>
   
<% } %>

HTML source
<form action="/UserProfile/SubmitUpdate" method="post">
    <input name="__RequestVerificationToken" type="hidden" value="saTFWpkKN0BYazFtN6c4YbZAmsEwG0srqlUqqloi/fVgeV2ciIFVmelvzwRZpArs" />
    <!-- rest of form goes here -->
</form>


At the same time, Html.AntiForgeryToken() will give the visitor a cookie called __RequestVerificationToken, with the same value as the random hidden value shown above

When the form is submitted, cookie and the hidden value are both sent to server.

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

[ValidateAntiForgeryToken]
public ViewResult SubmitUpdate()
{
    …..
}

Authorization filter that checks that:

·         The incoming request has a cookie called __RequestVerificationToken
·         The incoming request has a Request.Form entry called __RequestVerificationToken
·         These cookie and Request.Form values match

Assuming all is well, the request goes through as normal. But if not there’s an authorization failure with message “A required anti-forgery token was not supplied or was invalid”.

        [ValidateAntiForgeryTokenWrapper(HttpVerbs.Post)]
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult FarmingResults(FarmingRequestData data)
        {
            return this.GetFarmingResults(data);
        }
 
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 POSTactions are usually much more than controllers

 
    [ValidateAntiForgeryTokenWrapper(HttpVerbs.Post)]
    public partial class DivisionAdminController : SecureController
    {
        private const string DIVISION_VIEW = "Division";
        private const string MASTERLIST_VIEW = "MasterProductList";
       . . . .

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


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


File path: Source\PresentationFramework\Security\ValidateAntiForgeryTokenWrapperAttribute.cs

namespace PresentationFramework.Security
{
    [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)
        {
        }
.. . . . .  . .

Please refer the above file for complete code.
Due to above code, GET actions are not affected. Only HTTP POST requests are validated.

In case you want to protect multiple forms in your application independently of each other, you can use a “salt” value when you call Html.AntiForgeryToken(),
<%= Html.AntiForgeryToken("Some_String_Srinivas") %>


[ValidateAntiForgeryToken(Salt="Some_String_Srinivas")]
public ViewResult SubmitUpdate()
{
    // ... etc
}

Salt is just an random string. A different salt value means a different anti-forgery token will be generated. This means that even if an attacker manages to get hold of a valid token somehow, they can’t reuse it in other parts of the application where a different salt value is required.

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

Non-constant Salt in Runtime
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.

As mitigation, we pass Salt parameter in ValidateAntiForgeryTokenWrapperAttribute’s constructor

[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
            };
        }
. . . .

AJAX calls through jQuery

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.

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

function DeletePropertyProduct(state, gProductPriceID) {
    $.post(_deletePropertyFees, { State: state, ProductPriceID: gProductPriceID, __RequestVerificationToken: getAntiForgeryToken() },
              function (data) {
                  $("#PropertyFeesContainer").html(data);
              }
       );

To reuse the same, this can be encapsulated into a separate jQuery plugin as below (Reference: http://www.codeproject.com/Articles/267694/Security-in-ASP-NET-MVC-by-using-Anti-Forgery-Toke)

/// 
 
(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);

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);

 

$.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.});

Limitations of the Anti-Forgery helpers

1.    All genuine users must accept cookies

2.    It only works with POST requests, not GET requests

http://blog.stevensanderson.com/2008/09/01/prevent-cross-site-request-forgery-csrf-using-aspnet-mvcs-antiforgerytoken-helper/
http://www.codeproject.com/Articles/267694/Security-in-ASP-NET-MVC-by-using-Anti-Forgery-Toke

No comments: