Opening the Chrome console on one of our web apps I noticed this:

A huge amount of anti-forgery cookies with similar names, all valid for the same domain. These will be sent over the wire for every single request to that domain, as seen here:

Extra payload that won't be used for anything. What's going on here?

Digging in

First, I verified that the anti-forgery mechanism was working just fine. Removing all the cookies made it impossible to post requests to the server - very good. Logging in to the website again, a new anti-forgery cookie was created:

Notice that this cookie has the same name as one of the cookies in the previous image. I tried logging out and back in again, verifying that the same cookie name was reused - so no issues there. The cookies pile up because the cookie name changes. But why does it change, and when?

The site is implemented in ASP.NET Core, and after digging around on GitHub for a while I found this implementation in Microsoft.AspNetCore.Antiforgery:

options.CookieName = AntiforgeryOptions.DefaultCookiePrefix + ComputeCookieName(applicationId);

And in AntiforgeryOptions.cs:

public static readonly string DefaultCookiePrefix = ".AspNetCore.Antiforgery.";

Right, the cookie name is a combination of a default prefix and a computed suffix based on an applicationId parameter.

The internals of ComputeCookieName are interesting:

private static string ComputeCookieName(string applicationId)
{
  using (var sha256 = SHA256.Create())
  {
    var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(applicationId));
    var subHash = hash.Take(8).ToArray();
    return WebEncoders.Base64UrlEncode(subHash);
  }
}

A url-friendly base64-encoded, SHA256-hashed version of the applicationId string. Whatever applicationId is, someone wants to make sure it's not being leaked. Interesting!

But where does applicationId come from and why does it sometimes change?

"8 types of octopus"[1]

The next hint comes from the IOptions<DataProtectionOptions> being injected into AntiforgeryOptionsSetup in the same class. applicationId is set using its ApplicationDiscriminator property:

var applicationId = dataProtectionOptions.ApplicationDiscriminator ?? string.Empty;

DataProtectionOptions belongs to the Microsoft.AspNetCore.DataProtection package so that's the next place to look. It's set up by an internal class with a public Configure method

public void Configure(DataProtectionOptions options)
{
  options.ApplicationDiscriminator = _services.GetApplicationUniqueIdentifier();
}

Another injected service... let's keep digging. The _services instance is of type IServiceProvider and GetApplicationUniqueIdentifier is an extension method in the same package. Finally, this is the place the unique name is set:

discriminator = services.GetService<IHostingEnvironment>()?.ContentRootPath;

Really?! IHostingEnvironment.ContentRootPath? The unique part of the cookie name originates from the physical application path!

But does the physical path change? Oh yes, when using default settings in Octopus Deploy for all deploys to the web server it does. Octopus uses the package version in the physical path - which means that for every build there's a new physical path hosting the IIS website. Which again means that for every deploy we get a new IHostingEnvironment.ContentRootPath value, leading to a new cookie name. And when deploying several times a week - you get a pile of cookies!

This also explains the hashing of the cookie suffix seen above - you don't want your physical server path to leak out via the cookie name.

Solution

All this fuss for changing just one line of code? Yes... Set a static cookie name in startup.cs explicitly:

public void ConfigureServices(IServiceCollection services)
  {
    services.AddAntiforgery(opts =>
    {
      opts.CookieName = "AntiForgery.MyAppName";
    });

    (...)
}

This ensures that only one anti-forgery cookie is created for your site, not a new one for each deployed version.


  1. Silicon Valley S04E02. Just watch it. ↩︎