6

I basically need to protect against Cross Site Request Forgery in my Web API Controller that's a part of an MVC application. I am open to any ideas. At this point, I have an MVC View that displays an Esri map using ArcGIS for JavaScript API. The user creates a route on the map, and the route and various features it crosses can be saved via an AJAX POST. The View does not have a form. This is because all data that I POST to the server is in memory and not visible on screen (or in hidden fields).

So, I have the following in my MVC View to get antiforgery tokens:

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

I had this hidden input in the View, but realized this is bad since it has both the 'form token' and cookie token used with AntiForgery.Validation:

<input type="hidden" id="antiforgeryToken" value="@GetTokenHeaderValue()" />

Then, in a separate JavaScript file (not in script tag in View), I am doing a Http POST to my Web API Controller. This is where I add the tokens to the request headers:

var headers = {};
headers['RequestVerificationToken'] = $("#antiforgeryToken").val();
// Ajax POST to Web API Controller
$.ajax({
    async: true,
    url: '/api/RouteData',
    type: 'POST',
    headers: headers,
    data: requestData,
    dataType: 'json',
    error: function (xhr, statusText, errorThrown) {
        console.log('Error saving route data! ' + errorThrown);
    },
    success: function (result) {
    }
});

NOTE: the data that is getting POSTed in the body is all in memory inside a custom Dojo widget (since the page displays an Esri map using ArcGIS for JavaScript). There is not a form on the page since the user doesn't enter data.)

Tying it all together on the server side in a Web API Controller:

[System.Web.Http.HttpPost]
[ResponseType(typeof(RouteData))]
public async Task<IHttpActionResult> PostRouteData(RouteDataViewModel routeDataVM)
{
    try
    {
        HttpRequestMessage httpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
        ValidateRequestHeader(httpRequestMessage);
    }
    catch (Exception ex)
    {
        _logger.Log(LogLevel.Error, ex, ex.Message);
        throw;
    }
        // Now that we know user is who they say they are, perform update 
}

void ValidateRequestHeader(HttpRequestMessage request)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (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 is what validates the tokens.

I've seen this SO post, which has given me some ideas, but didn't quite solve the issue for me. A lot of credit is due to this post on ASP.net as well.

What makes this different for me (I think) is that my JavaScript is in a separate file and cannot call the server-side Razor function to get the antiforgery tokens, right? Any ideas on how to protect against CSRF without a form?

Robar
  • 1,929
  • 2
  • 30
  • 61
iCode
  • 1,254
  • 1
  • 13
  • 16

2 Answers2

2

CSRF protection is something that you should enact to do exactly that, protect from cross site request forgery.

A quick overview of CSRF is this:

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker's choosing.

The following would be an example of a CSRF attack against your Web API if it were unprotected:

<form action="http://yourapi.com/api/DeleteAccount">
    <input type="text" name="id" />
    <input type="submit" />
</form>

By posting this form from a compromised website, if you were logged into your Web API using cookie authentication (as an example) as an administrative account, it may well be able to delete accounts from your system.

This happens because when you're redirected to your API, your browser is passing across the cookie which is stored against the yourapi.com domain, thus authenticating the user and performing the required action. This attack vector is different from using, for example, a bearer token, to authenticate with your API as the third-party would have no knowledge of this token.

As you've rightly stated and implemented, the use of an anti-forgery token can protect from this as a generated token sent down to the client in a web response, and then sent back up by the client and validated to ensure that it is an allowed request from somewhere we expect. Like a bearer token, a third-party could have no knowledge of the token that you have sent down in your request from the server. In this respect, what you have implemented is absolutely correct and passing the token to the client is just how we would expect it to work.

In ASP.NET MVC (sans API) this is implemented as follows:

<form .. />
    @Html.AntiForgeryToken()
</form>

.. and validated server side with:

[HttpPost]  
[ValidateAntiForgeryToken]  
public ActionResult Action(MyModel model)
{
    // ..
}

When you call @Html.AntiForgeryToken(), the method both sets a cookie (containing the cookie token) and sends it down to the client, and also generates an <input type="hidden" ..> tag to send down to the client. These are then both sent back up and validated server side.

Armed with that knowledge, you can see that your concern about sending both the "cookie token" and "form token" is unfounded as this is what happens anyway.

I do see your cause for concern, but what you're trying to mitigate appears to be a Man in the Middle (MitM) attack rather than CSRF. To work around a large proportion of MitM attacks you should ensure that your site / API are both running over HTTPS. If your client is still susceptible to a MitM attack, your API would probably be of least concern to an attacker.

Rudi Visser
  • 21,350
  • 5
  • 71
  • 97
  • 1
    Your explanation is very helpful and thorough. I was able to wrap my mind around the MVC use of Html.AntiForgeryToken but got confused as to what I was actually protecting against when using AntiForgery.GetTokens because of the slightly different approach that method uses with the tokens. – iCode Jan 30 '17 at 16:04
  • 1
    No problem @iCode - If your Web API and MVC application are on the same domain, you can still use `@Html.AntiForgeryToken()` and pull out the form token via JS, the cookie token will of course be sent upon the next request by nature of being a cookie against that domain. Further details are from your linked answer: http://stackoverflow.com/a/4074289/698179 – Rudi Visser Jan 30 '17 at 16:08
  • 1
    Ah yes. My Web API and MVC app are on the same domain. Now I've come full circle in my understanding. Indeed I could create a simple form just for the purpose of generating the form token and the cookie (containing cookie token). It looks in that case I'd use JS to insert the form token into the data parameter on my AJAX post. I suppose this can be added in addition to the JSON object that I'm already setting as the data to POST. I'll test that out to learn both methods. – iCode Jan 30 '17 at 16:18
  • What to do if my web api and frontend are on a different domains.I am not able to pass token from web api to frontend.Is there any solution for this. – Diwas Poudel Jun 06 '19 at 04:51
1

After some discussion with others, it appears that the solution I've implemented is actually alright. Yes, both tokens are in hidden input fields. However, by the very nature of what CSRF is trying to protect against - another site trying to POST on your behalf - this solution works just fine. If I'm on my site, authenticated, and browse to another site that tries to make a POST on my behalf, the site will not have the necessary tokens.

iCode
  • 1,254
  • 1
  • 13
  • 16