Preventing CSRF Hacks in ASP.NET WebAPI

Posted by: Suprotim Agarwal , on 5/3/2013, in Category ASP.NET
Views: 34137
Abstract: Use ASP.NET MVC's AntiResourceForgery token mechanism and extend it to Web API via a delegating handler to prevent CSRF attacks

Sometime back Sumit Maitra wrote a nice article about what are CSRF attacks and how to prevent them in ASP.NET MVC. He demoed how a CSRF hack can be engineered and executed in details. He also showed us how ASP.NET MVC provides the AntiRequestForgeryToken to prevent CSRF hacks.

To quickly recap, a CSRF hack involves posting data to a valid URL that often requires authentication. The hack depends on the fact that an authentication token is available in the browsers cache and when the bad URL does a post, the browser uses the cached credentials.

If we had a WebAPI backed web application, the same loop holes exist. Fact is, WebAPI with Basic Authentication enabled, the hack gets a little easier to execute because Browsers caches Basic Auth tokens to prevent prompting for authentication for each resource they are getting.

 

The solution is to have an AntiForgeryToken style implementation in Web API backed pages as well. So today we will see how we can piggy back on the MVC AntiForgeryToken implementation to thwart CSRF attacks.

A Web API service with Basic Authentication

A simple example of Basic Authentication is Windows Authentication. Today, we’ll use a Windows Authentication enabled web site to explore Cross Site Request Forgery (CSRF) risks in Web API. To do so, first create a new Web API project in Visual Studio.

Let’s call it WebApiAuthorizationToken. Select the project node in Solution Explorer and change the following properties

- Anonymous Authentication: Set it to ‘Disabled’

- Windows Authentication: Set it to ‘Enabled’

setting-windows-authentication-in-project

Now all you have to do is use the Authorize keyword on either your Controller or Action Method to protect your API call. Correct? Well yeah, kind of!

Setting up a Simple POST to a WebAPI Controller

Traditionally we use WebAPI with AJAX calls but for sake of simplicity, let’s assume we’ll make a HTTP POST that will change some value at the server. We can imagine it to be something as sensitive as Money Transfer from one account to another.

In the Index.cshtml, let’s replace the section that says “We Suggest the following” with just a markup for a Button to post an HTML Form

<div>
    @using (Html.BeginForm("", "api/Values"))
    {
        <button id="post" name="post">Transfer Money</button>
    }
</div>

As we can see, the From will by default POST to the ‘api/Values’ controller. To make our POST method ‘secure’, we decorate it with the [Authorize] attribute. To test, we simply run the application and hit the “Transfer Money” button. We put a break point in the POST method to confirm that the form is posting correctly.

secure-post-to-web-api

Confirming Security

Just to confirm the security is indeed working, we go back to the project properties and reset the authentication to Anonymous and then run the app again. You will note that instead of navigating to the Post method, we are getting a download dialog like this

image

Let us start IE Developer Tools for the Index page, go to the ‘Network’ Tab and ‘Start Capturing’.

Now click on the “Transfer Money” button. You’ll see the following response

unauthorized-exception

So as we can see, our Authorize attribute is doing the trick. We are happy that our Web API is secure… umm… kind of!

Bad Guy Site and Cross Site Posting

In the real world, the Form to post will be much more elaborate. Now let’s assume there is a “Bad Guy” who has mimicked this Form and has somehow figured out that the api/values URL handles a POST action. So now all he has to do it get you to click on a button that POSTs his data instead of yours. Even though your data is secured via Basic authentication, if you had recently logged in to the original Site, the browser will see that you are posting to a site who’s Authentication you already have and will naively send the Authentication cookie along with the Bad Request. Let’ see how this is done.

Adding the “Bad Guy Site” to the Project

Let’s add a new Web API project to the Solution and now call it the ‘BadSite’

create-bad-site

Once the project is created we add the following in the Index.cshtml (again we’ll replace the suggestions section that comes by default).

<form action="http://localhost:57320/api/Values/" method="post">

<button id="post" name="post">Transfer Money</button>

</form>

Note the port number. This post is actually going to our original Site. Now build both the solutions and launch the Original Site. Next use the IIS Express tray icon to launch the BadSite in a different web browser as well

open-bad-site-from-iise

Now you have the scary scenario of both the sites looking exactly the same (except here the Bad Guy was nice enough to change the welcome banner).

identical-sites

Click on Transfer Money from the Original Site at port number 57320. Then click on the Transfer Money from the Bad Guy’s site at 57618. In both cases you will see the breakpoint in the /api/Values Post action method is hit. YIKES! Our so called Secure and Authorized site has been hacked into by a Bad guy.

Now let’s see what we can do to thwart the baddie.

Using an AntiForgeryToken to Prevent Cross Site Posting

Adding an AntiForgeryToken generates a Cryptographically valid hash at the server end which is split and a part is added as a hidden field, whereas the rest goes into a cookie. When data is posted, the Cookie and the Hidden Field are both sent back and if they are missing or they don’t match, the POST is rejected.

In MVC this happens automatically when you request for an AntiForgeryToken. In Web API, we have to do this check manually.

Enabling AntiForgeryToken

Update the Index.cshtml to request for an AntiForgeryToken as follows

<div>
@using (Html.BeginForm("", "api/Values", FormMethod.Post))
{
  @Html.AntiForgeryToken()
  <button id="postData" name="postData">Transfer Money</button>
}
</div>

Now when this from is posted back to Server, two things happen. We peek into the Request using Fiddler to see this

request-verification-cookie

clip_image001

Notice two things

1. Cookie called __RequestVerificationToken

2. Form Data with key __RequestVerificationToken

As I mentioned above, these tokens are cryptographically significant values and are essentially matching pairs. The AntiForgery.Validate(string 1, string2) helper method actually validates the pair at the server end once the data is posted. So even if our BadGuy was able to post his set of __RequestVerificationTokens, they would fail the Validation because they were generated for a different site.

Validating the Token

To validate the token on the server side, we have to do the following

1. Retrieve the cookie

2. Retrieve the FormDataCollection

3. Pickup the values for __RequestVerificationToken from both.

4. Pass them to Validate method

5. On error throw an Unauthorized exception

The Code in the Post Action method thus becomes like this

[Authorize]
// POST api/values
public void Post([FromBody]string value)
{
CookieHeaderValue cookie = Request.Headers
  .GetCookies(AntiForgeryConfig.CookieName)
  .FirstOrDefault();
if (cookie != null)
{
  Stream requestBufferedStream = Request.Content.ReadAsStreamAsync().Result;
  requestBufferedStream.Position = 0;
  NameValueCollection myform = Request.Content.ReadAsFormDataAsync().Result;
  try
  {
   AntiForgery.Validate(cookie[AntiForgeryConfig.CookieName].Value,
    myform[AntiForgeryConfig.CookieName]);
  }
  catch (Exception ex)
  {
   throw new HttpResponseException(
    new HttpResponseMessage(HttpStatusCode.Unauthorized));
  }
}
}

Now if the BadSite posts data to our Web API Method, they’ll get zich!

request-forgery-blocked

Generalizing the Solution using a Delegation Handler

If we wanted to generalize this solution, we could create a Delegation Handler and plug it into the Web API pipeline ensuring that all requests supply AntiForgeryToken details. Let’s quickly add a Handler and see how it works

1. Add a folder called Handlers in our Original Site

2. Add a new class called AntiForgeryTokenHandler and inherit it from DelegationHandler

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web;

namespace WebApiAuthorizationToken.Handlers
{
public class AntiForgeryTokenHandler : DelegatingHandler
{
  …
}
}

3. Next we override the SendAsync method of the DelegatingHandler.

protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
    CancellationToken cancellationToken)
{
return await base.SendAsync(request, cancellationToken);
}

4. We then move the code from the Post method to the Handler’s SendAsync Method and modify it to send HttpResponse instead of throwing Exception

protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
bool isCsrf = true;
CookieHeaderValue cookie = request.Headers
  .GetCookies(AntiForgeryConfig.CookieName)
  .FirstOrDefault();
if (cookie != null)
{
  Stream requestBufferedStream = request.Content.ReadAsStreamAsync().Result;
  requestBufferedStream.Position = 0;
  NameValueCollection myform = request.Content.ReadAsFormDataAsync().Result;
  try
  {
   AntiForgery.Validate(cookie[AntiForgeryConfig.CookieName].Value,
    myform[AntiForgeryConfig.CookieName]);
   isCsrf = false;
  }
  catch (Exception ex)
  {
   return request.CreateResponse(HttpStatusCode.Forbidden);
   }
  }
  if (isCsrf)
  {
   return request.CreateResponse(HttpStatusCode.Forbidden);
  }
  return await base.SendAsync(request, cancellationToken);
}

5. Now if we run the application and put a break point in the Handler, we will see that our application passes through the Handler and reaches the Post method, whereas the Bad Guy’s Post is returned back from the Delegation handler itself.

request-handled-by-delegation-handler

Whereas the bad guy gets to see this

bad-guy-forbidden

Caveats and Gotchas

One always has Caveats, don’t they. Well, in our case here is a small list of important ones

1. By depending on Cookies, you are assuming all your clients will be able to present Cookies. Remember Web API services are used to support different type of clients.

2. More importantly the Key/Value generated should be successfully validated by the AntiForgery library.

3. At one point above, we have read the Request’s Content stream and reset its Position to 0.

Stream requestBufferedStream = request.Content.ReadAsStreamAsync().Result;

requestBufferedStream.Position = 0;

This works when we are using the default ‘Buffered’ content type. However, if the defaults are changed to Streaming content type, then the Stream Position cannot be reset. As a result if you read the content here, no one else will be able to read it down the pipeline.

There are multiple solutions to this, one of the is to not use the Content; instead copy the Field from the RequestBody into a header while doing an AJAX POST. Then instead of reading the content, we pick the values from the custom header and do the validation.

The AJAX call would look something like this.

$.ajax(
{
cache: false,
dataType: 'json',
type: 'GET',
headers: { "X-RVT": $('input[name="__RequestVerificationToken"]').val() },
contentType: 'application/json; charset=utf-8',
url: '/api/Values',

}

The corresponding change to the Validate method would be.

AntiForgery.Validate(cookie[AntiForgeryConfig.CookieName].Value,
    request.Headers.GetValues("X-RVT").First());

Conclusion

We saw how we can expose a Cross Site Resource Forgery vulnerability if we were not careful with our public web services specially when using Basic Authentication. One way to plug the whole was to use ASP.NET MVC's AntiResourceForgery token mechanism and extend it to Web API via a delegating handler.

Another solution of course would be use a more robust security mechanism like OAuth.

Download the entire source code of this article (Github)

Give me a +1 if you think it was a good article. Thanks!
Recommended Articles
Suprotim Agarwal, ASP.NET Architecture MVP, MCSD, MCAD, MCDBA, MCSE, is the CEO of A2Z Knowledge Visuals Pvt. He primarily works as an Architect Consultant and provides consultancy on how to design and develop .NET centric database solutions.

Suprotim is the founder and primary contributor to DotNetCurry, SQLServerCurry and DevCurry. He has also written an EBook 51 Recipes using jQuery with ASP.NET Controls.

Follow him on twitter @suprotimagarwal


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by Nilesh Joshi on Monday, May 13, 2013 7:31 PM
Great article.. !! Thanks
Comment posted by Mike Wasson on Monday, June 3, 2013 2:12 PM
Have you see the anti-forgery handler in the MVC4 SPA template? See http://www.asp.net/single-page-application/overview/introduction/knockoutjs-template
Comment posted by Suprotim Agarwal on Saturday, July 6, 2013 12:20 AM
Mike: Thanks for the link
Nilesh: You are welcome :)
Comment posted by Hardy Wang on Wednesday, July 10, 2013 1:35 PM
I don't understand, why not just decorate the postback method with [ValidateAntiForgeryToken]?
Comment posted by Dany on Tuesday, August 20, 2013 2:41 AM
Thank you, interesting article. I would like to know if it is possible to set an expiration date for AntiForgeryToken  value (eg. Cookie token authentication requires to be changed every 2 months)
Comment posted by Mike on Friday, March 14, 2014 6:55 AM
thanks

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel