David
David

Reputation: 218877

ValidateAntiForgeryToken in an ASP.NET Core React SPA Application

I'm trying to use the framework's tools to add some simple CSRF validation to an ASP.NET Core React SPA. The application itself is essentially a create-react-app setup (a single index.html with a root element and everything else is loaded in from bundled JavaScript).

Tinkering with some information found on links such as this one, I've set the following in my Startup.ConfigureServices:

services.AddAntiforgery(options => options.Cookie.Name = "X-CSRF-TOKEN");

And confirmed in my Chrome tools that the cookie is being set. If I omit the above line, a cookie is still set with a partially randomized name, such as: .AspNetCore.Antiforgery.RAtR0X9F8_w Either way the cookie is being set. I've also confirmed that any time I re-start the whole application the cookie value is updated, so the framework is actively setting this cookie.

Observing network requests in my Chrome tools, I confirm that the cookie is being sent to the server on AJAX request. Placing a breakpoint on the server and observing the Request.Cookies value in a controller action also confirms this.

However, if I decorate any such AJAX requested action with [ValidateAntiForgeryToken] then the response is always an empty 400.

Is there a configuration step I've missed somewhere? Perhaps the action attribute is looking in the wrong place and I need to use a different validation?

Upvotes: 10

Views: 17504

Answers (2)

johnrom
johnrom

Reputation: 672

The accepted answer here is extremely incorrect when it suggests to send both cookies via JS-readable cookies:

// do not do this
context.Response.Cookies.Append("X-CSRF-TOKEN", tokens.CookieToken, new CookieOptions { HttpOnly = false });
context.Response.Cookies.Append("X-CSRF-FORM-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });

If you send both the Cookie token and the Request token in a Cookie that is readable by JS, you are defeating the purpose of having a Cookie token and a Request token.

The purpose of using both tokens is to make sure that

  • you have a valid session (the HTTP-only Cookie proves this),
  • you have requested a form from the site using this valid session (the HTTP-readable Cookie or another method can prove this), and
  • you are submitting the form from the same valid session

Why It's Wrong.

The Request Token

The Request Token ensures that you have actually loaded a page (example.com/example-page). Think about this: if you are logged in to example.com as an administrator, a request from anywhere from your browser (where CORS allows the necessary properties) can successfully validate against Cookie-based CSRF Validation and your authentication.

However, by adding the Request Token, you are confirming that your browser also actually loaded a request to the form (or at least, the site) before submitting it. This is usually done with a hidden input. This is automatically done by using the Form Tag Helper in Asp.Net.

<form action="/myEndpoint" method="POST">
  <input name="__RequestVerificationToken" type="hidden" value="@antiforgery.GetAndStoreTokens(context).RequestToken" />
  <button type="submit">Submit</button>
</form>

It can also be set .. anywhere. like window.CSRFRequestToken, and manually added to a POST request, like in this fetch example:

fetch('/myEndpoint', { method: 'POST', headers: { 'X-XSRF-Token': window.myCSRFRequestToken, 'Bearer': window.mySuperSecretBearerToken } };

The Cookie Token

In the above contrived example, the user is logged in via a bearer token via OAuth or something (not recommended, use HTTP-only Cookies in a browser environment).

The Cookie Token ensures that a malicious script cannot exfiltrate your Request Token and send requests on your behalf. Without it, in a supply chain attack, a malicious user can send your secrets to a malicious actor:

window.addEventListener('load', => sendMySuperSecretInfoToTheShadowRealm(window.CSRFRequestToken, window.mySuperSecretBearerToken));

Now the malicious user could send a request from wherever they want using your CSRF and bearer token to authenticate. BUT! Not if you have your good friend HTTP-only Cookie-based CSRF Validation -- because JavaScript cannot read HTTP-only cookies.

The Solution

Asp.Net combines these solutions by setting both a Cookie Token and a Request Token. Therefore, when you are sending a request to AspNet you send both:

The cookie:

Cookies.Append('X-CSRF-Token', @antiforgery.GetAndStoreTokens(context).CookieToken);

and either the aspnet form helper tag:

<form action="myEndpoint" />

or manually print the token:

<form action="myEndpoint" asp-antiforgery="false">
    @Html.AntiForgeryToken()
</form>

or provide the token manually to your scripts:

window.myCSRFRequestToken = "@antiforgery.GetAndStoreTokens(context).RequestToken)";
fetch('/myEndpoint', { method: 'POST', headers: { 'X-CSRF-Token': window.myCSRFRequestToken };

Don't take my word for it

Please please read this page fully in case I didn't explain anything clearly:

https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-6.0

A final note:

In the documentation above, the very last example uses a cookie to send the request cookie. This is very different in a subtle way than the answer here. The accepted answer sends both cookies as Javascript-readable { HttpOnly = false }. This means JavaScript can read both and a malicious user can read both and craft a special request themselves that will validate against both Cookie and Request CSRF validations (where CORS allows).

In the documentation, one is sent via an HTTP only cookie (this cannot be read by JS, only used for Cookie-based CSRF validation) and the other is sent via an HTTP-readable cookie. This HTTP-readable cookie MUST be read by JavaScript and used with one of the above methods (form input, header) in order to validate CSRF Request Token Validation.

Upvotes: 11

itminus
itminus

Reputation: 25350

I just inspect the log and find out there's an exception:

Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery cookie ".AspNetCore.Antiforgery.HPE6W9qucDc" is not present. at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext) at Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter.OnAuthorizationAsync(AuthorizationFilterContext context)

It indicates that you forgot to configure the cookie name :

   public void ConfigureServices(IServiceCollection services)
   {
       //services.AddAntiforgery();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

       // In production, the React files will be served from this directory
       services.AddSpaStaticFiles(configuration =>
       {
           configuration.RootPath = "ClientApp/build";
       });
   }

So I just add a configuration as below :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAntiforgery(o => {
            o.Cookie.Name = "X-CSRF-TOKEN";
        });
        // ...
    }

and it works now.

Also, if you would like to omit the line of services.AddAntiforgery(options => options.Cookie.Name = "X-CSRF-TOKEN"); , you can use the built-in antiforgery.GetAndStoreTokens(context) method to send cookie:

   app.Use(next => context =>
    {
        if (context.Request.Path == "/")
        {
            //var tokens = antiforgery.GetTokens(context);
            var tokens = antiforgery.GetAndStoreTokens(context);
            context.Response.Cookies.Append("X-CSRF-TOKEN", tokens.CookieToken, new CookieOptions { HttpOnly = false });
            context.Response.Cookies.Append("X-CSRF-FORM-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
        }
        return next(context);
    })

Both should work as expected.

Upvotes: 11

Related Questions