Sunday, September 9, 2012

Prevent Cross-Site Request Forgery (CSRF) in MVC web application using AntiForgeryToken() helper


Cross-site scripting (XSS) is the number one security issue on the web. Your application can be vulnerable to cross-site request forgery (CSRF) attacks not because you the developer did something wrong, but simply because of how the whole Web is designed to work. Basically web browsers allow GET and POST requests to be made between different web sites. Cross-site request forgery (CSRF) occurs when a user visits a malicious web page that makes their browser send requests to your application that the user did not intend. This can be done with the src attribute of the IMG, IFRAME or other tags and more complicated requests, including POSTs, can be made using JavaScript. Because the browser always sends the relevant cookies when making requests, requests like this appear to originate from an authenticated user. The malicious site isn’t able to see the results of these requests, but if create, update or delete functionality can be triggered; the malicious site may be able to perform unauthorized actions.

Example of CSRF works:

Here is your controller class (“ResultsController”). The user goes to Index(), which renders the form. Now they post that form to view the results ([HttpPost] method), which can do some useful staff i.e., modify user details, bank accounts details etc. Still now all looks good.

public class ResultsController : Controller
{
    //
    // GET: /Results/
    public ActionResult Index() { return View(); }

    [HttpPost]
    public ActionResult Index(ResultsModel model)
    {
        //do some staff
        return View(model);
    }
}

But that controller is easily target for CSRF. How? Let’s check the html generated by the controller below.

<form method="post" action="/results">
   User name: <input type="text" value="someusername" name="UserName" id="UserName"/>
   Email address: <input type="text" value="email@email.com" name="Email" id="Email"/>
  <input type="submit" value="Update">
</form>
   


The following shows the “HTTP POST” that is sent to the website (URL: http://localhost:51582/results). Here you can notice the following things:
  • We are posting to a well known URL, with or without sending a cookie.
  • .ASPXAUTH, which lets the site know that user is already logged in or not
  • We are posting some data (UserName=someusername&Email=email%40email.com)
The code is very much simple. Just creating a form and posting the data to do some useful works. So why we are fighting for CSRF? Now check below. Another guy can write the following piece of code:

<html>
<head>
    <title></title>
</head>
<body>
    <form method="post" name="csrf" action="http://localhost:51582/results">
    <input type="hidden" name="UserName" value="someusername" />
    <input type="hidden" name="Email" value="hereis@your.email" />
    </form>
    <script type="text/javascript">
        document.csrf.submit();
    </script>
</body>
</html>

That person just did nothing only create a HTML page hosted somewhere in the world with two input types, the same values and write a small script to post the form and he can successfully update those things.

How to block CSRF:

We can do the following things:

  • We can check the incoming request's “Referer” header to prevent this attack. That posted HTML is exactly the same as the form is created except the “Referer” because it is hosted somewhere in the world not in that server. But some proxy server, some people can disable their browser’s “Referer” header for privacy. So this is not a good solution at all.
  • We can put a token as a hidden filed inside the form & then check that the correct value was submitted or not. It is good to use a random generated value while submitting the form. So that third party cannot forge the valid form post.

The AntiForgeryToken helpers:

ASP.Net MVC introduces html helpers to detect and block CSRF using tokens. AntiForgeryToken is built in validation and simple to add in the form (it is a hidden input element with token). We need to add an @Html.AntiForgeryToken() into the form.

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div>
        UserName: @Html.TextBoxFor(m => m.UserName)
        Email: @Html.TextBoxFor(m => m.Email)
    </div>
    <input type="submit" value="Update" />  
}

This will generate a token and writes it inside the form. HTML will be like this:

<form method="post" action="/results">
<input type="hidden" value="TKjmpi-KfyhZZf79x-v0BzIN6W-hDL9DhUAZJE5RffDw6ZRDGMPajlrGWSV8a7oP5oXrNXGw1riZ7FE86CO3to6i2rfpZ6_KoAHWEOUCwmFlL6-beCKZYKbf1pxL3xLnnEQ4qrTggZoDrxwPrB0AeEFeu8wJukwL0SB2iwi55oU1"
    name="__RequestVerificationToken">
<div>
    UserName: <input type="text" value="" name="UserName" id="UserName">
    Email: <input type="text" value="" name="Email" id="Email">
</div>
<input type="submit" value="Update">
</form>

This (AntiForgeryToken) generated a visitor cookie name “__RequestVerificationToken”, with the same value as the random hidden value.



Now, to validate the incoming form post, need to add the “[ValidateAntiForgeryToken]” to your action method.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(ResultsModel model)
{
    //do some staff
    return View(model);
}

This will validate the token, when the form post is submitted, compares the cookie value to the submitted verification token.

The simple and excellent mechanism of AntiForgeryToken:

a.  The server writes tokens to cookie and inside the form.b. When the form is submitted to server, token in cookie and token inside the form are sent in the HTTP request.c. Server validates the tokens.

Multiple forms in application & wrapper class:

To protect multiple forms in one controller we can easily avoid a large number of “[ValidateAntiForgeryToken]” attributes. We can use the following wrapper class, 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); }
   }
}

We can call that like following:

[ValidateAntiForgeryTokenWrapper(HttpVerbs.Post)]
public class ResultsController : Controller
{
// All GET: actions, which are not affected.
// All POST: actions, those requests are validated.
}

Add the token in the AJAX request:

When AJAX Post, we can include the verification token manually. We can simply add the parameter “__RequestVerificationToken” with the token from the form as follow:

    $.ajax({
        type: "POST",
        url: URL, //this will be the url
        data: { value: value, __RequestVerificationToken: getVeryficationToken() },
        success: function (result) {
            // Success
        }
    });

    function getVeryficationToken() {
        var inputElements = $("input");
        for (var i = 0; i < inputElements.length; i++) {
            var inputElement = inputElements[i];
            if (inputElement.name === "__RequestVerificationToken") {
                return inputElement.value;
            }
        }
        return null;
    }

Some helpful articles:

No comments:

Post a Comment