Html.AntiForgeryToken – Balancing Security with Usability

comments

When writing forms for your ASP.Net MVC websites the common approach to ensuring only real people use them site is to simply add an Html.AntiForgeryToken() to your form’s view mark-up and controller and be on your way. I've recently found out this approach while simple, can actually have pretty serious affects on both how your visitors use of your site as well as their view of it’s professionalism and stability – two things you really don’t want any trouble with.

Sign up growth == Startup  Secret Sauce

In June of last year I set out to save ASP.Net web developers time and frustration by automating their website’s deployment – I launched OnCheckin.com.

OnCheckin is my first Software as a Service (SAAS) based startup and I'm learning new things about visitor behaviour all the time – one of the most critical problems in startup marketing and user experience design is actually getting users to *use* your service.

Actually getting people to signup, use your service and tell their friends is the fastest way to get the word out. It’s no coincidence that this needs to be a seamless process if it’s to be a success.

Because of this every step of your signup process needs to work.

Flawlessly.

If someone gets stuck somewhere along the signup journey, they’ll often give up right there and be on their way.The hardest thing dealing with when users leave is that you almost always have very little information as to why they bugged out. If your signup process is amazing but something you’re unaware of turns the user off you’ve often lost them forever.

As a developer, if your signup forms *break* you should definitely know about it with exception handling and tracking.

When I launched OnCheckin I added a number of tools and frameworks to the project to allow me to better track application exceptions;

  • NLog for general logging and emails on critical application exceptions (including web exceptions).
  • Elmah for filtering and email of web exceptions.

The above frameworks are pretty standard tools in my toolbox for building stuff as they allow you to easily be notified when something is going wrong.

And this brings me to using the ASP.Net MVC Html.AntiForgeryToken().

image

Stop the Evil Robot/XSS Attacker/Lazy user Horde

The ASP.Net AntiForgeryToken system is pretty neat. It delivers protection by creating a dynamic lock to submitting your forms by generating a token for each form page a user downloads that has an AntiForgeryToken in it. This token is then set as an encrypted session cookie.

The form field the server then sends out is an encrypted key to this lock.

<input name="__RequestVerificationToken" type="hidden" value="4dv-…...PVQIvpXNE..KZyV1DCjeN1..rmtMDJ9fQ2">

When users submit this hidden form field the server compares it with the encrypted cookie to see if you’re using the same key it sent you with the page.

The main reasons that you use Antiforgery tokens in your web applications can really be summarised into the following:

  • Cross-Site Request Forgery or XSS
    We want to make sure that someone hasn’t followed a poison frame, form or link to end up submitting your form without knowledge.
  • Avoiding webspider or basic automation
    A number of spam spiders are well developed to simply surf the net looking for forms to submit with spammy information. These spiders are usually not that advanced and often don’t support cookies and/or JavaScript. You’ll often see these on blog comments (“SIGNUP TO GET A 6 PACK IN 5 MINS WITH ACAI BERRIES!!1”). This falls into the same category as above with the Cross-Site Scripting/Request forgery – the spider is usually submitting values to your form without first loading any dynamically generated components like your validation token cookie.
  • Ensuring application state
    Sometimes we don’t want our users to have multiple windows open at the same time, or be able to hit the back button because they are in a workflow. The AntiForgeryToken html helper will generate a new one token for every form page requested and store it in a session cookie – you can therefore only have one antiforgery tokenised page open per domain as a second page will change the content of the validation token cookie and break all open pages but the last tab loaded.

This is great, and the Microsoft ASP.Net MVC team have done a great job of making this CSRF nonce easy to generate and use.

There a few situations where using the AntiForgeryToken helper can cross the line into bad visitor usability in order to achieve these goals of adding security.

Broken: Application restart

The AntiForgery tokens rely on the application’s underlying Machine validation key to check the signed nature of the validation cookie. The default installation of an ASP.Net website commonly changes this key on application pool restart/recycle. This means that anytime your application pool restarts or your server admin runs iisreset the validation key changes breaking any form posts that occur after your application restart. This can make even second-long downtimes a potential big deal.

Change of identity during form completion

The validation code that runs against an AntiForgeryToken also checks your logged in user credentials haven’t changed – these are also encrypted in the cookie.

This means that if you logged in or out in a popup or another browser tab, your form submission will fail with the following exception;

System.Web.Mvc.HttpAntiForgeryException (0x80004005):
The provided anti-forgery token was meant for user "", but the current user is jblogs@website.com.

You can turn this off by using this little guy.

AntiForgeryConfig.SuppressIdentityHeuristicChecks = true;

Machine key reliance

Because the validation of an AntiForgery token relies on both the Machine validation key AND the Machine encryption key, when running your server in a load balanced scenario you need to remember to manually set the machine keys for your websites to the same value across servers – even if you’re using sticky sessions as you never know when you’ll be shutting down one machine and the users may be migrated during session drainage by some brands of load balancer.

… But none of these compare to the biggest fallout I experience from the use of AntiForgery tokens;

Breaking the back button

When a user submits a form but gets an invalid result, you may be surprised to hear how often they simply hit the back button – even if you’ve provided validation messaging on the next page’s form.

This is a huge no, no. As although you haven’t broken the back buttons behaviour by stopping it or changing it, you have broken the users journey by not allowing them to back track and make changes to their submission.

Even in 1999 breaking the back button was considered the number one web design mistake.

In fact a study was done by the University of Wichita showing how common a user behaviour deciding to hit the back button was when faced with site navigation vs. the browser bar. They actually came out pretty even.

image

The problem is that when a user submits your form the validation token it is validated by your server and then the token session cookie is altered.

When users hit the back button the cached copy of the old form you had loaded for them now contains a hidden __RequestVerificationToken form field with an old value that won’t validate when your server reads the cookie on the next form submission.

Ponder this user flow;

  1. User completes form and submits.
  2. User is shown form error message and hits back.
  3. User changes values in form and submits.
  4. User is shown site wide error page and hits back.
  5. User tries to submit form again.
  6. User is shown site wide error page and hits back.
  7. User tries to submit form again.

There is literally no way out of the loop without the user understanding how the AntiforgeryToken works (“JUST REFRESH THE PAGE ALREADY!!!!1”).

The last thing I want to do is stop a valid user successfully using my site unless it is absolutely necessary.

Especially for OnCheckin – by stopping a user from signing up or sending in a support request. I may end up losing them.

Trying to limit the user experience damage from an AntiForgeryToken

Optimising for “forward-only” navigation

So we know one thing from the above; users like the back button.

But depending on what your application does there are ways to limit their compulsion to do so. The simplest of which is to do your best to try and make the visitor not feel like they need to “rewind” their request to alter it.

  • Ensure that form error messaging is clear and concise to ensure the user knows what is wrong. Contexual errors give bonus points.
    image
  • Always maintain form state between form submissions. Apart from passwords or credit card numbers, there’s no excuse thanks the to MVC form helpers.
    @Html.LabelFor(x => x.FirstName)
  • If forms are spread across tabs or hidden divs such as those used in SPA frameworks like Angular or ember.js, be smart and show the controller layouts or form that the errors actually originated from in the form submission when displaying the error. Don’t just direct them to the home controller or first tab.

“What’s going on?” - Keeping the user informed

When a AntiForgeryToken doesn’t validate your website will throw an Exception of type System.Web.Mvc.HttpAntiForgeryException.

If you’ve been good boy/girl you’ve got friendly errors turned on and this will mean your error page will show.

image

This isn’t really the best approach, as not only does it stop your website visitor from doing what they set out to do in submitting your form, but it makes them believe is was an error on your site!

You’ve successfully stopped a visitor from using your site and you’ve crushed their confidence in your site’s stability.

You can make this a little easier by at least giving the user a more informative page targeted at these exceptions by catching the HttpAntiForgeryException.

private void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();

    if (ex is HttpAntiForgeryException)
    {
        Response.Clear();
        Server.ClearError(); //make sure you log the exception first
        Response.Redirect("/error/antiforgery", true);
    }
}

image

Removing Html.AntiForgeryToken from pages that make up your funnel

This is a controversial one for some, but as a business owner I see any technology that creates a barrier for entry for your visitors is unacceptable.

Most websites have a transactional funnel for important visitor tasks. Sign up, checkout, submit payment.

image

Some website actions must have CSRF and XSS protection as they’re prone to attack, examples of these:

  • User account password & preference forms.
  • Credit card forms.
  • User generated content forms (comments and posts).
  • Forms that conduct atomic actions (send email, release funds, launch a rocket ship).

Examples of forms where CSRF and XSS can be judged on a case-by-case basis as you can cleanse the data post completion for dodgy entries:

  • Register forms for membership.
  • Support request forms (nothing worse that an getting error when reporting one).
  • Shopping cart checkout.
  • Registration of interest (if you must, Captcha can cover you for compliance).
  • Anything marketing based where dropouts are high (facebook connect login etc.)

In the above examples, it is critical that your tech doesn’t get in the way of a user action.

Do as I say, not as I do.

In summary, first hand experience running a start-up has opened my eyes to how critical signup flow is to a business or website that revolves around user growth and conversion.

Because of this I've either removed or made the decision to stop using @Html.AntiForgeryToken() on the OnCheckin site wherever I want to ensure a pain free journey. I may review this at a later stage, but for now while the service is small I’m more interested in people signing up (even if they do so through cross-site scripting) than I am in hindering them in coming on board.

On the pages where I still use AntiForgery tokens I implement customised error pages to better inform users as to what’s occurred as well – to the users who tried to use OnCheckin early and were hit by a generic error page when resubmitting a form and couldn’t figure out why; accept my dearest apologies, the site and I would love a second try :-)