Csrf Protection With Aspnet Core And Blazor Week 29
remi bourgarel
Posted on July 22, 2018
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);
}
}
- 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>();
And enable XSRF protection via headers on the “ConfigureService” method
services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
});
- 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]
- 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 };
});
- 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;
}
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();
}
}
The DI settings are set in Program.cs in my client project like this
configure.Add(new ServiceDescriptor(
typeof(IBrowserCookieService),
typeof(BrowserCookieService),
ServiceLifetime.Singleton));
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;
}
- 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
Posted on July 22, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024