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

2

9

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

Unfathomable answered 29/7, 2017 at 12:53 Comment(17)
Antiforgery token uses timeout on cookies, so this could be the reason.Gawky
@TasosK. I've been monitoring this and it seems to be unrelated to timeouts - I have successfully validated tokens after days and have seen failures after seconds or minutes.Unfathomable
@Unfathomable have you tried disabling caching on the login method?Latoyia
not sure this will help a great deal but can you clarify you are using the latest web api, i.e. Web API 2.2 - 5.2.3 ?Forgery
@AleksandarMarkovski - I haven't. Is this something applied at the WebAPI Controller, or MVC? The user logs in through a WebAPI POST request.Unfathomable
@Forgery I am indeed on 5.2.3Unfathomable
@Unfathomable it is a stretch but try adding a {[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")]} attribute on the login method you use.Latoyia
You say you see it occasionally.. could it be that the AF token is actually doing it's job and protecting you against compromised sessions? Alternatively, is your session timeout extending on each API call or maybe just some of them causing the intermittent fails?Ornamental
@Ornamental - this is happening fairly frequently with a low number of sessions so there's virtually no chance of compromise (small personal project). The timeout.... My web.config has timeout="1440" path="/" slidingExpiration="true" set for the authentication entry. It's tough because I only have logs to work with; I wonder if there's anything I can do log-side to provide more insights...Unfathomable
have you noticed if this occurs after the second post you execute after a get of the whole page?Reformation
@CiroCorvino -that's what seems to happen. GET works and then POST fails.Unfathomable
the first POST it's ok, the second fails, do you confirm?Reformation
@CiroCorvino - oh no; first fails. Apologies, I misunderstood.Unfathomable
@Unfathomable did you tried to add Machine Key to your webconfig and verified it ?Ascham
@Webruster yes sirUnfathomable
@Unfathomable did you tried using with MessageHandlerAscham
@Unfathomable - In an earlier comment, you had mentioned you only had logs to look at. You can setup (its free for now at the basic level) Application Insights from Azure azure.microsoft.com/en-us/services/application-insights in your app and it may give more insight detail into the failed request than what you are getting now.Luigiluigino
S
4

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.

Stane answered 8/8, 2017 at 12:57 Comment(6)
Oh wow - I didn't know that. I love when the solution is to delete stuff.Unfathomable
If that solution helped you, do not hesitate to validate ;-)Stane
I'm researching a bit further now - it seems like the header may be required but the value does not require validation? I'm seeing some conflicting guidance here: security.stackexchange.com/questions/167064/…Unfathomable
The "guidance" says "You use the basic javascript XMLHttpRequest object to do GET or POST request. You are vulnerable since this is possible to do the equivalent request from a cross site using html form" which is absolutely erroneous. I will update my answer and include a code snippet so you can see by yourself.Stane
Update done. You can trust ASP.NET developers, but nobody said that you should ;-DStane
I upvoted your answer for useful info into it, but it doesn't explain why happens as described in the OPReformation
R
0

I try to give an answer the same, also if in the comments we exchange, yours it seems a not related scenario with mine..

A such type of issue can be due to the XMLHttpRequest.setRequestHeader() behaviour, because this function "combines" the values of an header that has been already assigned in the context of an http request, as stated by MDN and Whatwg:

If this method is called several times with the same header, the values are merged into one single request header.

So, if we have a SPA for example that executes all ajax POSTs setting a given http header, in your case:

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

the first ajax POST request sets a clear header ("X-XSRF-Token") and so, server side, you should have a "valid" header value to compare to.

But, in absence of a page refresh, or a new GET request, all subsequent ajax POSTs, as well as stated in the MDN and Whatwg documentation, will make a dirty assignment of the same header ("X-XSRF-Token"), because they combine the new values with the olds.

To avoid this issue, you could try to reset "X-XSRF-Token" value (but there isn't much documentation on that and it seems a not reliable solution...)

beforeSend: function (request) {
     request.setRequestHeader("X-XSRF-Token", null);      //depends on user agents..
     //OR.. request.setRequestHeader("X-XSRF-Token", ''); //other user agents..
     //OR.. request.setRequestHeader("X-XSRF-Token");     //other user agents..
     request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}

Other solutions can rely on some client-side state handing mechanism that you have to implement on your own, because it is not possible to get values or state access of the http request headers (only response headers can be accessed).

Update - revision of the following text: So, if we have a SPA for example that executes all ajax POSTs recycling the XMLHttpRequest object for each calling and setting a given http header, in your case: ...

Reformation answered 4/8, 2017 at 16:20 Comment(4)
This can't be it. Whenever you create a new XMLHttpRequest object it has an empty list of headers associated with it.Chronic
From the OP it isn't quite clear if the XMLHttpRequest get instantiated every time or once..Reformation
New request every time so I don't think this is it :/Unfathomable
Ok.. But try to add a console.log in the body of the function to see if it get called more then once, and also check in the extended filter onActionExecuting what get returned from the header tokenReformation

© 2022 - 2024 — McMap. All rights reserved.