Harden Anti-Forgery Tokens with IAntiforgeryAdditionalDataProvider in ASP.NET Core
Niels Swimberghe - - .NET
Follow me on Twitter, buy me a coffee
Anti-Forgery tokens are a common technique to prevent Cross-Site Request Forgery (XSRF/CSRF) attacks. ASP.NET Core uses a hidden field to store the anti-forgery token and uses the ValidateAntiForgeryToken
attribute to validate the token. As the token is sent to the browser in a hidden field, it is also stored in an HttpOnly cookie. When the form is submitted, ASP.NET Core compares the hidden field value with the cookie value and rejects the HTTP request if it doesn't match.
To learn how to use the out of the box anti-forgery technique, read the Microsoft Documentation: "Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core".
Hardening anti-forgery tokens #
A couple of years ago, an external company scanned many of our products and websites for vulnerabilities. One of the issues that arose many times was the following: "The CSRF token in the body is validated on server side but is not revoked after use even though the server generates a new CSRF token."
With other words, the anti-forgery tokens generated by ASP.NET Core can be reused multiple times which increases the risk if your anti-forgery tokens are somehow leaked.
Luckily, ASP.NET Core provides a way to extend the anti-forgery tokens using IAntiforgeryAdditionalDataProvider
.
As the interface's name suggests, you can implement this interface to provide additional data to your anti-forgery token which you can validate on form submission. Here's a dummy implementation:
using System; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; namespace AntiForgeryStrategiesCore { public class DummyAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider { public string GetAdditionalData(HttpContext context) { return "Some dummy additional data"; } public bool ValidateAdditionalData(HttpContext context, string additionalData) { return additionalData == "Some dummy additional data"; } } }
In GetAdditionalData
you can generate whatever you want to add to the anti-forgery token. You also receive the HttpContext
as a parameter so you can use advantage of the properties on the context such as Request
, Session
, Cookies
, etc.
In ValidateAdditionalData
you put in the logic to validate your additional data. If invalid, return false; if valid return true.
For ASP.NET Core to start using your implementation, you need to register it with the dependency injection in Startup.ConfigureServices
like this:
// existing code omitted for brevity using Microsoft.AspNetCore.Antiforgery; public class Startup { // existing code omitted for brevity public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IAntiforgeryAdditionalDataProvider, DummyAntiforgeryAdditionalDataProvider>(); // existing code omitted for brevity } // existing code omitted for brevity }
You can find the source code for these sample on this GitHub repository.
With this extensibility point, you can resolve the security issue raised earlier in many ways. For example, you can generate a random token and store the token in Session
. When the token is validated, you can immediately remove the token from session as you can see in the example below:
using System; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; namespace AntiForgeryStrategiesCore { public class SingleTokenAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider { private const string TokenKey = "SingleTokenKey"; public string GetAdditionalData(HttpContext context) { var token = TokenGenerator.GetRandomToken(); context.Session.SetString(TokenKey, token); return token; } public bool ValidateAdditionalData(HttpContext context, string additionalData) { var token = context.Session.GetString(TokenKey); context.Session.Remove(TokenKey); return token == additionalData; } } }
The advantage is that you can only use the token once and it will be invalidated. The disadvantage is that you cannot open multiple forms at the same time. Every time a form is requested, a new token is generated and overrides the previous token. Only the latest request form will pass the anti-forgery token validation.
This could be a potential UX nightmare.
To allow multiple forms to be requested and submitted, you could store multiple generated additional tokens. The following implementation stores a comma-separated list in Session
. To prevent the session variable from becoming to large, the list is limited to 3 items in this case. When the list becomes too large, the oldest token is removed from the list. When a token is submitted for validation, that token is removed from the list.
Here is the source:
using System; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; using System.Linq; using System.Collections.Generic; namespace AntiForgeryStrategiesCore { public class QueueTokensAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider { private const string TokenKey = "QueueTokensKey"; private const int AmountOfSessionTokens = 3; private const char Separator = ';'; public string GetAdditionalData(HttpContext context) { var newToken = TokenGenerator.GetRandomToken(); if (newToken.Contains(Separator)) { newToken = newToken.Replace(Separator.ToString(), string.Empty); //to prevent collision } List<string> existingTokens = GetTokens(context); if (existingTokens.Count == AmountOfSessionTokens) { existingTokens.RemoveAt(0); } existingTokens.Add(newToken); SetTokens(context, existingTokens); return newToken; } public bool ValidateAdditionalData(HttpContext context, string additionalData) { var tokens = GetTokens(context); if (tokens.Contains(additionalData)) { tokens.Remove(additionalData); SetTokens(context, tokens); return true; } return false; } private static void SetTokens(HttpContext context, List<string> existingTokens) { context.Session.SetString(TokenKey, string.Join(";", existingTokens)); } private static List<string> GetTokens(HttpContext context) { return context.Session.GetString(TokenKey)?.Split(';').ToList() ?? new List<string>(); } } }
You could also store an expiration time as additional data. On validation, you can see if the time has expired or not. Unless you are a sadist, just make sure the expiration time is sufficiently long like multiple hours.
The following example expires the token within 10 seconds for testing purposes:
using System; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http; namespace AntiForgeryStrategiesCore { public class TimeTokenAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider { public string GetAdditionalData(HttpContext context) { return DateTime.Now.AddSeconds(10).ToString(); } public bool ValidateAdditionalData(HttpContext context, string additionalData) { var expirationDateTime = DateTime.Parse(additionalData); return DateTime.Now < expirationDateTime; } } }
Summary #
Using IAntiforgeryAdditionalDataProvider
you can harden ASP.NET Core's anti-forgery token feature by adding additional data and validating the additional data.
This security scan prompted me to ask this question on StackOverflow which I ended up answering myself. If this helped you out, feel free to upvote the question and answer.