Search code examples
c#asp.netasp.net-mvccsrfantiforgerytoken

Antiforgery tokens are reusable


We use ASP.NET MVC's default Antiforgery technique. Recently a security company did a scan of a form and made note that they could use the same _RequestVerificationToken combination (cookie + hidden field) multiple times. Or how they put it: "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."

After reading the documentation and multiple articles on the implementation of Antiforgery, it is my understanding that this is indeed possible as long as the session user matches the user in the tokens.

Part of their recommendation: "Such tokens should, at a minimum, be unique per user session" In my understanding this is already the case, except for anonymous users, correct?

My questions: Is this a security issue? How much of a risk is it? Is there a library that makes sure tokens are not reusable/invalidated.

If not, including an extra random token in session that will be reset on every request sounds like it would solve the issue.


Solution

  • The customer eventually agreed that the Antiforgery implementation of ASP.NET is sufficient. Just for fun I wanted to extend Antiforgery to meet the invalidation requirement.

    The Antiforgery library has one extensibility point: IAntiforgeryAdditionalDataProvider (Core) and IAntiForgeryAdditionalDataProvider (pre-Core)

    In ASP.NET MVC (pre-Core) you can set this on startup.

    using System.Web;
    using System.Web.Helpers;
    // ...
    
    namespace AntiForgeryStrategiesPreCore
    {
        public class MvcApplication : HttpApplication
        {
            protected void Application_Start()
            {
                // ...
                AntiForgeryConfig.AdditionalDataProvider = new MyAdditionalDataProvider();
            }
        }
    }
    

    For ASP.NET Core you need to register your IAntiforgeryAdditionalDataProvider as a service. If you don't it will use the DefaultAntiforgeryAdditionalDataProvider (source) which does nothing.

    using System;
    using Microsoft.AspNetCore.Antiforgery;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    
    namespace AntiForgeryStrategiesCore
    {
        // ...
    
        public class Startup
        {
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSingleton<IAntiforgeryAdditionalDataProvider, SingleTokenAntiforgeryAdditionalDataProvider>();
                // ...
            }
        }
    
        // ...
    }
    

    Now you can add additional data to your Antiforgery token which will be encypted into your cookie and and form field. Here's a MVC Core example that holds on to a single token in Session, and removes it after usage.

    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;
            }
        }
    }
    

    This isn't recommended because when you open multiple tabs with multiple forms, only one of the forms will have the valid token in session, and the other will fail. That's why I made one that holds on to multiple tokens. You can find that AdditionalDataProvider and others on GitHub (Time based, Queue based).