Creating a mandatory Anti-Forgery token

One of the things I love about ASP.Net, is that a lot of the hard work that is required for a creating a secure website has already been done for you. It usually only takes a couple of lines of code to add these features in which means there are no excuses for missing off important security measures.

Anti-Forgery Token

One of these features is the Anti-Forgery token and it can be added to your MVC website with just 2 lines of code. So what is an anti-forgery token? As the name suggests it is a token to prevent forgery! In the same way that someone might forge a signature to pretend to be someone else, it is possible for a malicious person to forge a request to your website without the request coming from your website.

So how is this done I hear you say? Well lets say you have a form on your website for changing user details such as name and email address, and a hacker wanted to change these to something else.

The hacker could create a form on another website which matches the request your website is expecting and post to the same URL. The entire form could be in hidden fields and posted via an Ajax request on page load making it invisible to the user.

If the user is already logged in to your website when the other website posts the form, your website treats it as a valid request and will change the user details to whatever the hacker wants.

So how do we get around this?

Well in the same way that 2 factor authentication works on something you know and something you have. The anti-forgery token works as the something you have (sorry about the poor analogy). The server places a hidden field with a populated anti-forgery token into your form. When a request is made to your website, the server checks for the presence of the anti-forgery token and if it doesn’t exist or doesn’t match the expected value an exception occurs.

As the hackers malicious form doesn’t know what the Anti-Forgery token is the request fails.

Adding an Anti-Forgery token

This all sounds great so how do you add this in? As i mentioned you only need 2 lines of code to add in an Anti-Forgery token, one in the view and one in the controller.

View

In the view you need to add in the anti-forgery token with @Html.AntiForgeryToken()  inside your post form like this:

@using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{      @Html.AntiForgeryToken()
.....
...

Controller

The controller then needs to have the [ValidateAntiForgeryToken] added to the post action.

// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{      if (!ModelState.IsValid)      {            return View(model);      }

If you look at the source code of your page you should now see the following in your form (with a different value of course):

<input name="__RequestVerificationToken" type="hidden" value="JN8mexoJ6sCyfy9TzagXr1DSmjk6au-5VfP9IN_EyLhkWwvd-w2HGJ5EzCW1e_W9nf3wpTQWG_bDgDFFhzLWU8EqAb_8uQtXTwvojSTe3541">

Ok so lets test it by changing the value in this hidden field and seeing what happens.

<input name="__RequestVerificationToken" type="hidden" value="bad token">

anti-forgery_exception

As shown above we get the expected exception that the anti-forgery token could not be decrypted. In production you will have custom errors turned on so the user will see a nice error message instead of a stack trace which is dangerous in itself.

Making the Anti-Forgery token mandatory

As you might have guessed from the title of this post, we aren’t finished yet. If you are working in a team it is possible that someone might forget to add the anti-forgery token into their new view and action. So what happens if you forget to add the token to the view?

anti-forgery-not-present-exception

As you would expect you get an exception saying that the token is missing. Great, but what happens if you forget to add the ValidateAntiForgeryToken to the action?

Nothing. No exception. It just allows it through.

Given that you have to add an attribute to every post action it is conceivable to think one could be missed.

We can mitigate this issue by doing 2 things:
1.) Use an HTML Helper Extension for BeginForm to always add in the token.
2.) Write a unit test to check for the presence of the ValidateAntiForgeryToken on every Post action.

So how do we do this:
1.) Use an HTML Helper extension for BeginForm
To create a HTML Helper extension we are going to create a new class called HtmlExtensions and place it somewhere in our project. You then need to add the following method:

public static MvcForm BeginFormWithToken( this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes)
{
    var form = htmlHelper.BeginForm(actionName, controllerName, routeValues, method, htmlAttributes);
htmlHelper.ViewContext.Writer.WriteLine(htmlHelper.AntiForgeryToken());
    return form;
}

Now I have only created one for the BeginForm method shown above but you will probably want to create one for each of the other overloads you use. You will then need to go through each of your views and change BeginForm to BeginFormWithToken. So now we have the Anti-Forgery token being added to every form.

2.) Write a unit test to check for the presence of the ValidateAntiForgeryToken
To do this we have to use reflection to find all the controller actions with HttpPost and check that they have a ValidateAntiForgeryToken present. In the example below you will need to change MyWebApplication to the name of your Web project.

[TestMethod]
public void TestValidateAntiForgeryTokenAttributeOnAllPostActions()
{
    // Act
    var actions = Assembly.Load("MyWebApplication").GetTypes()
                        .Where(t => typeof(Controller).IsAssignableFrom(t))SelectMany(type => type.GetMethods()).Where(method => method.IsPublic &&
                        method.GetCustomAttributes(typeof(HttpPostAttribute), true).Any() &&
                        !method.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), true).Any()).ToList();
    // Assert
    Assert.IsFalse(actions.Any(), actions.Any() ? $"The action '{actions[0].Name}' in the '{actions[0].DeclaringType.Name} ' controller is missing the ValidateAntiForgeryToken attribute." : string.Empty);
}

This test will fail if any of the actions are missing the validate attribute and will give the name of the first offender. Feel free to modify this. For example you might want to get it to print out all of the actions that are missing the Anti-Forgery token attribute.

If this post was helpful please leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *