Search code examples
asp.net-mvc-4cookieshttp-postverificationantiforgerytoken

How does AntiForgeryToken, specifically, 'match up' with a cookie?


We have a web page, and it has a form on it. There is an antiforgerytoken added to this MVC page's form via the helper:

@Html.AntiForgeryToken()

We realised today that we have the page cached, which we thought would be a massive issue, but multiple machines may submit the same form, even though they share the same verification token in the page's source!?

This is unexpected as far as I'm aware, I thought the idea was that the same verification token can be found in the cookie? I'm obviously misunderstanding the mechanism behind the token (both on the page and the amendment of a user's cookie).

Can somebody explain to me how this page still works? More specifically, how the server validates the form's post request.

I thought it was as simple as the server checking that the token string was identical to a token string found in the cookie.

NB: Caching is turned off for now fwiw, we're not 100% happy with caching a page with tokens on it now that we're a bit more clued up on it.


Solution

  • We realised today that we have the page cached, which we thought would be a massive issue, but multiple machines may submit the same form, even though they share the same verification token in the page's source!?

    Yes, caching is a problem when it comes to anti-forgery tokens. You have two choices:

    1. Don't cache the form.
    2. Use the VaryByCustom property of OutputCache to vary by the token itself.

    I don't have code to hand with me, so I'll show you an example which is taken from this article:

    In order to avoid this issue you can use the VaryByCustom property on the OutputCache attribute:

    [OutputCache(
      Location = OutputCacheLocation.ServerAndClient,
      Duration = 600,
      VaryByParam = "none",
      VaryByCustom = "RequestVerificationTokenCookie")]
    public ActionResult Index()
    {
      return new View();
    }
    

    And then program the rule on the global.asax‘s GetVaryByCustomString method:

    public override string GetVaryByCustomString(HttpContext context, string custom)
    {
      if (custom.Equals("RequestVerificationTokenCookie", StringComparison.OrdinalIgnoreCase))
      {
        string verificationTokenCookieName =
          context.Request.Cookies
            .Cast<string>()
            .FirstOrDefault(cn => cn.StartsWith("__requestverificationtoken", StringComparison.InvariantCultureIgnoreCase));
        if (!string.IsNullOrEmpty(verificationTokenCookieName))
        {
          return context.Request.Cookies[verificationTokenCookieName].Value;
        }
      }
      return base.GetVaryByCustomString(context, custom);
    }
    

    Edit per comments

    The reason it works is because it's only comparing the value of the cookie to the value of the generated hidden field on the form. There isn't a list of tokens maintained on the server, ready to be validated, if you thought it might work that way. That means, as far as the client is concerned, despite the generation of the form being cached on the server, the client is still receiving a 'new' form and, consequently, a cookie to go along with that, so the comparison won't fail.