Improving security with unit tests and reflection
Back in November of 2014,
Phil Haack published his
Avoid async void methods
blog post—it contained two useful things: a warning about an
antipattern (I have to admit to not knowing about the
distinction between
async void
and
async Task
methods before), and a clever application of unit tests and
reflection to enforce best practices in a project. As it turns
out, that pattern can be repeatedly useful! Hooray for
metaprogramming!
Cross-site Request Forgery (hereon, CSRF) is a common attack vector for web sites. Roughly speaking, CSRF attacks can execute requests on a vulnerable site A when a user (who is authenticated at site A) visits malicious site B. The OWASP link does a much better job explaining CSRF (with examples!), so I would recommend reading it for a more detailed explanation. William Zeller and Edward Felten’s paper “Cross-Site Request Forgeries: Exploitation and Prevention” is also worthwhile, though dense, reading.
Luckily for us, ASP.NET MVC (since MVC 3) comes with a built-in
way to mitigate CSRF attacks, using
ValidateAntiForgeryTokenAttribute
and
Html.AntiForgeryToken()
. The implementation behind it is the “double-submitted token”
approach, in which an
HttpOnly
cookie is combined with a value embedded inside the request
(usually a form, but may be AJAX). However, developers are prone
to forgetfulness—it’s especially easy to forget to add
[ValidateAntiForgeryToken]
to action methods that take a POST.
After reading Phil’s post and thinking about this some, I
realized that I could use the same approach (a unit test that
reflects over types/methods) to enforce this in our own code by
making the test fail if it found a method that could take a
POST, but wasn’t decorated with
[ValidateAntiForgeryToken]
. We’re working with ASP.NET 5/MVC 6, so the test code is
xUnit, but it should be easily adaptable to NUnit, MSTest, or
any other testing framework.
Without any further ado, here’s the test, with some type names changed to protect the innocent:
using Microsoft.AspNet.Mvc;
using System;
using System.Linq;
using Xunit;
[Fact]
public void All_post_actions_should_validate_antiforgery_token()
{
var controllerTypes = typeof(MyController).Assembly.GetTypes()
.Where(type => typeof(Controller).IsAssignableFrom(type))
.ToList();
var postActions = controllerTypes.SelectMany(type => type.GetMethods())
.Where(m => Attribute.GetCustomAttribute(m, typeof(HttpPostAttribute)) != null)
.ToList();
var failingActions = postActions.Where(m => Attribute.GetCustomAttribute(m, typeof(ValidateAntiForgeryTokenAttribute)) == null)
.ToList();
var numFailing = failingActions.Count();
var message = string.Empty;
if (numFailing > 0) {
var ending = numFailing > 1 ? "s:" : ":";
var actions = string.Join(", ", failingActions.Select(a => $"{a.DeclaringType?.Name}!{a.Name}"));
message = $"{numFailing} failing action{ending} {actions}";
}
Assert.False(numFailing > 0, message);
}
Hope this helps someone else out there! I’m hoping to be able to apply this pattern to other things as well.
Leave a comment