Preventing Cross-Site Request Forgery (CSRF) Attacks in WebAPI

November 11, 2013 by Anuraj

.Net ASP.Net MVC Javascript Web API

CSRF is an attack which forces an end user to execute unwanted actions on a web application in which he/she is currently authenticated. With a little help of social engineering (like sending a link via email/chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. A successful CSRF exploit can compromise end user data and operation in case of normal user. If the targeted end user is the administrator account, this can compromise the entire web application.

Here is an example of CSRF attack.

  1. User requesting a page - with Forms authentication enabled and Anonymous authentication disabled. Server is checking for Forms authentication cookie. And responding with a 302 status code, as the request doesn’t contains the cookie. Requesting a web page with Forms authentication
  2. Browser is redirecting to the login page, and gets the login page. Browser redirecting to login page
  3. User authenticating with the credentials, sending a POST request. credentials send a POST request for authentication
  4. Server authenticates the user and Forms authentication cookie is set. Server Authenticated the user and cookie is set
  5. User sending a Ajax POST request to WebAPI. And resource created, server responded with status 201. Here is the controller action.
[Authorize]
public HttpResponseMessage Post(Employee employee)
{
    var employeeId = _employeeRepository.Create(employee);
    var response = Request.CreateResponse<Employee>
        (HttpStatusCode.Created, employee);
    response.Headers.Location =
        new Uri(Url.Link("DefaultApi", new { id = employeeId }));

    return response;
}

Here is the request

function createEmployee() {
    var emp = { Name: "anuraj", Email: "anuraj@server.com", Phone: "938944" };
    $.ajax("/api/Employee", {
        type: "POST",
        contentType: "application/json",
        data: JSON.stringify(emp),
        dataType: "json"
    });
}

POST request to Web API The controller action contains an Authorize attribute, which means only Authorized request will be accepted.

  1. From another web page, user sending another POST request, without logged in.

Here is the request. Please not the URL, in previous request it was /api/Employee, but in this request, it is absolute url.

function createEmployee() {
    var emp = { Name: "attack", Email: "attack@fake.com", Phone: "attack" };
    $.ajax("http://localhost:56103/api/Employee", {
        type: "POST",
        contentType: "application/json",
        data: JSON.stringify(emp),
        dataType: "json"
    });
}

And here is the server response.

POST request to WebAPI without authentication

It is also created the resource, and server responded with status 201. As the request comes to an authenticated web page, the browser will automatically send the cookie with the request.

Typically, CSRF attacks are possible against web sites that use cookies for authentication, because browsers send all relevant cookies to the destination web site. However, CSRF attacks are not limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user logs in with Basic or Digest authentication. the browser automatically sends the credentials until the session ends.

Anti-Forgery Tokens - To prevent CSRF attacks ASP.NET MVC uses Anti-Forgery Tokens or request verification tokens. If you enable this, server will includes two tokens with the response. One token is sent as a cookie. The other is placed in a hidden form field. 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.

Here is the Anti-Forgery Token implementation for Ajax requests, this function will generate the cookie token and form token, and returns as string.

public string TokenHeaderValue()
{
    string cookieToken, formToken;
    AntiForgery.GetTokens(null, out cookieToken, out formToken);
    return cookieToken + ":" + formToken;
}

And you can use this in Ajax requests like this.

function createEmployee() {
    var emp = { Name: "anuraj", Email: "anuraj@server.com", Phone: "938944" };
    $.ajax("/api/Employee", {
        type: "POST",
        contentType: "application/json",
        data: JSON.stringify(emp),
        dataType: "json",
        headers: {
            'RequestVerificationToken': '@TokenHeaderValue()'
        }
    });
}

And in server side, I created an Action Filter, which will validate the request.

public override void OnActionExecuting(HttpActionContext actionContext)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (actionContext.Request.Headers.
        TryGetValues("RequestVerificationToken", out tokenHeaders))
    {
        string[] tokens = tokenHeaders.First().Split(':');
        if (tokens.Length == 2)
        {
            cookieToken = tokens[0].Trim();
            formToken = tokens[1].Trim();
        }
    }
    AntiForgery.Validate(cookieToken, formToken);
}

AntiForgery.Validate method to validate the tokens. The Validate method throws an exception if the tokens are not valid.

And here is the server response with and without AntiForgery implementation.

POST request with RequestVerification header

POST request with RequestVerification header

And POST request without RequestVerification header - Server response is 500, Internal server error, ActionFilter attribute is throwing exception.

POST request without RequestVerification header

You can also prevent CSRF attacks by verifying the request referrer too, to a certain extend.

Happy Programming

Copyright © 2024 Anuraj. Blog content licensed under the Creative Commons CC BY 2.5 | Unless otherwise stated or granted, code samples licensed under the MIT license. This is a personal blog. The opinions expressed here represent my own and not those of my employer. Powered by Jekyll. Hosted with ❤ by GitHub