Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The anti-forgery cookie token and form field token do not match when using WebApi

I have a single-page app (user loads a bunch of HTML/JS and then makes AJAX requests without another call to MVC - only via WebAPI). In WebAPI I have the following:

public sealed class WebApiValidateAntiForgeryTokenAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(
        System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (actionContext == null)
        {
            throw new ArgumentNullException(nameof(actionContext));
        }
        if (actionContext.Request.Method.Method == "POST")
        {
            string requestUri = actionContext.Request.RequestUri.AbsoluteUri.ToLower();
            if (uriExclusions.All(s => !requestUri.Contains(s, StringComparison.OrdinalIgnoreCase))) // place some exclusions here if needed
            {
                HttpRequestHeaders headers = actionContext.Request.Headers;

                CookieState tokenCookie = headers
                    .GetCookies()
                    .Select(c => c[AntiForgeryConfig.CookieName]) // __RequestVerificationToken
                    .FirstOrDefault();

                string tokenHeader = string.Empty;
                if (headers.Contains("X-XSRF-Token"))
                {
                    tokenHeader = headers.GetValues("X-XSRF-Token").FirstOrDefault();
                }

                AntiForgery.Validate(!string.IsNullOrEmpty(tokenCookie?.Value) ? tokenCookie.Value : null, tokenHeader);
            }

        }
        base.OnActionExecuting(actionContext); // this is where it throws
    }
}

Registered in Global.asax:

    private static void RegisterWebApiFilters(HttpFilterCollection filters)
    {
        filters.Add(new WebApiValidateAntiForgeryTokenAttribute());
        filters.Add(new AddCustomHeaderFilter());
    }

Occasionally, I see the The anti-forgery cookie token and form field token do not match error in my logs. When this is happening, both tokenCookie.value and tokenHeader are not null.

Clientside, all of my AJAX requests use the following:

beforeSend: function (request) {
     request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
},

With Razor generating the token once on my SPA page:

@Html.AntiForgeryToken()

I have my machine key set in Web.config.

What could be causing this?

Update I just checked logs and I'm seeing this sometimes as well:

The provided anti-forgery token was meant for user "", but the current user is "[email protected]". a few seconds ago

This occurs when a user refreshes their instance of the SPA while logged in. The SPA then drops them into the landing page instead of the inner page for some reason (User.Identity.IsAuthenticated is true) - then they can't log in because of this error. Refreshing pulls them back inside. Not sure what this means, but I figured more info can't hurt.

Appendix https://security.stackexchange.com/questions/167064/is-csrf-protection-useless-with-ajax/167076#167076

like image 888
SB2055 Avatar asked Jul 29 '17 12:07

SB2055


1 Answers

My answer will recommend to not try to use CSRF protections based on tokens in AJAX calls, but rather to rely on the native CORS features of the web browser.

Basically, any AJAX call from the browser to the back-end server will check for the domain origin (aka the domain where the script was loaded from). If the domains match (JS hosting domain == target AJAX server domain) the AJAX calls performs fine, otherwise returns null.

If an attacker tries to host a malicious AJAX query on his own server it will fail if your back-end server has no CORS policy allowing him to do so (which is the case by default).

So, natively, CSRF protections are useless in AJAX calls, and you can lower your technical debt by simply not trying to handle that.

More info on CORS - Mozilla Foundation

Code example - use your console inspector!

<html>
<script>
function reqListener () {
  console.log(this.responseText);
}

var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "http://www.reuters.com/");
oReq.send();
</script>
</html>

Run it and look at the Security error:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://www.reuters.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

Mozilla is pretty clear regarding the Cross-site XMLHttpRequest implementation:

Modern browsers support cross-site requests by implementing the Web Applications (WebApps) Working Group's Access Control for Cross-Site Requests standard.

As long as the server is configured to allow requests from your web application's origin, XMLHttpRequest will work. Otherwise, an INVALID_ACCESS_ERR exception is thrown.

like image 104
Fabien Avatar answered Oct 02 '22 21:10

Fabien