DEV Community

remi bourgarel
remi bourgarel

Posted on • Originally published at remibou.github.io on

Csrf Protection With Aspnet Core And Blazor Week 29

CSRF protection with ASPNET Core and Blazor

CSRF is a well know web app vulnerability, you can find more about it here https://scotthelme.co.uk/csrf-is-dead/. As you can see on the title, this vulerability is dead. But it’s dead for everyone with an up to date browser. So we have to implmeent classic protections until every browser has implemented the protection and everyone has updated his browser.

In this article I’ll show you how I implemented it with my Blazor / ASPNET Core app calles TOSS

Server side (ASPNET Core 2.1.1)

Usually CSRF protection works this way :

  • browser renders a form with a token in an hidden field
  • user submit the form
  • server validate the field is on the client request and validate it

But in a SPA, forms are not created on server side so we need an other way. The one I’ll use is the following :

  • Server sends a non http cookie with the validation token (so it can be read from javascript)
  • client read the cookie and will send its value in an header for every http request (ajax) it’ll execute

On the server side I need an ASPNET Core middleware for setting the cookie if not present

public class CsrfTokenCOokieMiddleware
    {
        private readonly IAntiforgery _antiforgery;
        private readonly RequestDelegate _next;

        public CsrfTokenCOokieMiddleware(IAntiforgery antiforgery, RequestDelegate next)
        {
            _antiforgery = antiforgery;
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            if(context.Request.Cookies["CSRF-TOKEN"] == null)
            {
                var token = _antiforgery.GetTokens(context);
                context.Response.Cookies.Append("CSRF-TOKEN", token.RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
            }
            await _next(context);
        }
    }
Enter fullscreen mode Exit fullscreen mode
  • Middleware doesn’t have to implement a specific interface, just declare this method “async Task InvokeAsync(HttpContext context)”, don’t know why the .Net core team didn’t securized it with an interface though
  • I use the built-in IAntiforgery service for generating the value as I have no idea how it’s supposed to be built. When we are talking about security we have to avoid, as much as we can, in-house code.

Then I register this Middleware in my Configure method on Startup.cs

app.UseMiddleware<CsrfTokenCOokieMiddleware>();
Enter fullscreen mode Exit fullscreen mode

And enable XSRF protection via headers on the “ConfigureService” method

services.AddAntiforgery(options =>
            {
                options.HeaderName = "X-CSRF-TOKEN";
            });
Enter fullscreen mode Exit fullscreen mode
  • If you don’t add this, the CSRF protection will only look for field on the request

In all the method I want to protect (mostly POST, as GET have no impact on the system) I add the following attribute

[ValidateAntiForgeryToken]
Enter fullscreen mode Exit fullscreen mode
  • There might be a way to avoid this for all methods but I don’t know it.

Client Side (Blazor 0.4)

On the client side, first I need to read the cookie’s value (both are hosted on the same domain, so when I load my app the cookie is supposed to be set). For reading the cookie value I’ll have to use Js interop because there is no method in Blazor for reading the cookies.

Blazor.registerFunction("getDocumentCookie", function () {
    return { content: document.cookie };
});
Enter fullscreen mode Exit fullscreen mode
  • I use a container for my result because the string serialization/deserialization in Blazor is buggy

Then in C# I read this value

public static string GetCookie()
        {
            StringHolder stringHolder = RegisteredFunction.Invoke<StringHolder>("getDocumentCookie");           
            return stringHolder.Content;
        }
Enter fullscreen mode Exit fullscreen mode

I created a service for parsing this value

/// <summary>
    /// Service for reading the current cookie on the browser
    /// </summary>
    public class BrowserCookieService : IBrowserCookieService
    {
        /// <summary>
        /// returns the cookie value or null if not set
        /// </summary>
        /// <param name="cookieName"></param>
        /// <returns></returns>
        public string Get(Func<string,bool> filterCookie)
        {
            return JsInterop
                .GetCookie()
                .Split(';')
                .Select(v => v.TrimStart().Split('='))
                .Where(s => filterCookie(s[0]))
                .Select(s => s[1])
                .FirstOrDefault();
        }
    }
Enter fullscreen mode Exit fullscreen mode

The DI settings are set in Program.cs in my client project like this

configure.Add(new ServiceDescriptor(
   typeof(IBrowserCookieService),
   typeof(BrowserCookieService),
   ServiceLifetime.Singleton));
Enter fullscreen mode Exit fullscreen mode

Then I add the headers on all my http calls like this (I use a wrapper around HttpClient for avoiding copy/paste)

private HttpRequestMessage PrepareMessage(HttpRequestMessage httpRequestMessage)
  {
      string csrfCookieValue = browserCookieService.Get(c => c.Equals("CSRF-TOKEN"));
      if (csrfCookieValue != null)
          httpRequestMessage.Headers.Add("X-CSRF-TOKEN", csrfCookieValue);
      return httpRequestMessage;
  }
Enter fullscreen mode Exit fullscreen mode
  • I can use HttpClient DefaultHeaders but I couldn’t find a nice place to initialize it.
  • And it works :)

I think this way of implementing CSRF is quite usual for SPA, but if there is a security problem please tell me.

Reference

Discussion (2)

Collapse
kingleo10 profile image
Niroj Dahal • Edited

Instead of writing [ValidateAntiForgeryToken] as action attribute in all POST methods , you can configure Startup to auto validate for all POST methods. This is what I did in ConfigureServices method:

        services.AddMvc(options =>
        {
            options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());

        })
Collapse
rembou1 profile image
remi bourgarel Author

Thanks, but this would add a check for every request while I don't think this kind of check is necessary everywhere in an app, but it's safer to add it everywhere.