Thursday, July 9, 2015

Cross Site Request Forgery in Single Page Applications

Cross Site Request Forgery in Single Page Applications

Recently, I presented security in MVC Applications at SoCal Code Camp. There I talked about Cross-Site Request Forgery Attack as I have discussed in previous blog post here.
As everybody is moving to Single Page Applications (SPA) these days, somebody in the audience asked me about how to prevent CSRF attack in SPA application. So today's blog post is dedicated to understand how to prevent a CSRF attack in SPA application.

Setting up SPA Application

In my example, I have created a simple SPA application using ASP.NET MVC and AngularJS. I am simply retrieving an employee record and allowing the user to edit and save it into the database.

Clicking on the save button simply makes the POST Request:

$scope.saveEmployee = function () {

$http.post('/api/employee', {

 'employeeId': $scope.employeeId, 'Name': $scope.name,
 'department': $scope.department, 
 'division': $scope.division

})

    .success(function (data, status, headers, config) {

 }).error(function (data, status, headers, config) {

    });

 };

The POST Request passes all the data like employeeId, Name, Department and Division to the EmployeeController.cs POST Action method which saves it to the database.

The POST Request requires user to be authenticated and have the valid session. So any unauthorized user can't edit Employee records without being authenticated. So we are safe there.

Now imagine a user is authenticated and he opens another tab and visits another website in same browser which tries to submit a similar POST request with bad data. Since his session is valid, the request will go through. So the victim has been fooled into some unwanted action without his knowledge.

In case of regular MVC applications, we can simply add AntiforgeryToken to our razor view and decorate our action method with [ValidateAntiForgeryToken] and that will prevent us from such cross site request forgery attacks. However, in case of SPA everything is happening on AJAX requests. So we need to add our antiforgery tokens to our POST request explicitly.

Anti-forgery Token in SPA

In order to use the token in our POST request, we need to add it to our view first. So I created a hidden field on my View:

<input id="antiForgerySpaToken" 
data-ng-model="antiForgerySpaToken" 
type="hidden" 
data-ng-init=
"antiForgerySpaToken='@GetSpaAntiforgeryToken()'" />

In .NET we have this method AntiForgery.GetTokens() to generate the tokens for us. So I added a javascript function which gets the tokens from server and adds it to the View.


<script>

@functions{
public string GetSpaAntiforgeryToken()

{

   string cookieToken, formToken;

   AntiForgery.GetTokens(null, out cookieToken, 
      out formToken);

   return cookieToken + ":" + formToken;

}

}

</script>

So this will get the Antiforgery tokens from the server and put it on the page.

Now on every POST request we will be submitting the token for verification. So we add the token to the header like this:

 $scope.saveEmployee = function () {

    $http.post('/api/employee', {

    'employeeId': $scope.employeeId, 

    'Name': $scope.name,

    'department': $scope.department, 

    'division': $scope.division

}, {headers: { 'RequestVerificationSpaToken': 
$scope.antiForgerySpaToken }})

    .success(function (data, status, headers, config) {

}).error(function (data, status, headers, config) {

});

};

So now the token is being passed in the request header. At the server side we need to verify whether the token is authentic or not.

In Web API, all the HTTP Requests pass through various handlers before they reach the controller. I created a Delegating Handler which verifies whether the token is correct or not.
If the token is not correct, we know that the request is not valid.

public class AntiForgerySpaTokenHandler : DelegatingHandler

{

private string SpaTokenName = "RequestVerificationSpaToken";


protected override async Task<HttpResponseMessage> 
   SendAsync(HttpRequestMessage request,

     CancellationToken cancellationToken)

{

     if (request.Method != HttpMethod.Get)

     {

         try

         {

             IEnumerable<string> vals;

             if (request.Headers.TryGetValues(SpaTokenName,
               out vals))

             {

                 var cookieToken = String.Empty;

                 var formToken = String.Empty;

                 var tokens = vals.First().Split(':');

                  if (tokens.Length == 2)

                 {

                     cookieToken = tokens[0].Trim();

                      formToken = tokens[1].Trim();

                 }

                 AntiForgery.Validate(cookieToken, formToken);

             }

         }

     catch (Exception e)

     {

      return request.CreateResponse(HttpStatusCode.Forbidden);

     }

    }

    return await base.SendAsync(request, cancellationToken);

 }

}


We need to add this handler to the MessageHandlers List. So In WebApiConfig.cs, I added:

config.MessageHandlers.Add(new AntiForgerySpaTokenHandler());

So here if the tokens do not match, the Handler prevents the request from being propagated to Controller and sends a Forbidden response.
So now if the attacker tries to send the POST request, it will not have the valid tokens atleast and that will prevent us from CSRF attacks.

Conclusion

So we saw how CSRF is a different beast in SPAs. In future, hopefully .NET will make this easier to implement. But until then we need to explicitly take care of this.
For future updates to my weekly blog, please subscribe to my blog via the "Subscribe By Email" feature at the right and follow me on Twitter. Until then Happy Coding :)



No comments:

Post a Comment