Using NUnit to Ensure Post Methods Validate Anti-Forgery Tokens
Anti-Forgery Token Attributes
Microsoft’s article here
provides a great overview of preventing cross-site request forgery attacks with the use
of the built-in ValidateAntiForgeryTokenAttribute
.
One tool we can use to ensure that all of our controller methods decorated with the
HttpPostAttribute
are also decorated with the ValidateAntiForgeryTokenAttribute
is to write a unit test case.
Example Method
In our HomeController
, we have a method that is marked with a post
verb:
public class HomeController : Controller
{
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Create(PostViewModel postedModel)
{
// do something...
return View();
}
}
We want to ensure that every new post
verb method is also decorated with the
ValidateAntiForgeryToken
attribute. We could accomplish this with an administrative
rule (coding guidelines) or rely on code reviews to catch when methods are not properly decorated,
or we can write one test that will check every post
method in all controllers within an assembly.
The Unit Test
In our unit test project, we create a single test that ensures that a method is decorated
with the ValidateAntiForgeryToken
attribute:
[TestCaseSource(typeof(AllControllerPostMethods))]
public void PostMethods_validate_antiforgery_token(MethodInfo method)
{
Assert.IsTrue(method
.GetCustomAttributes(
typeof(ValidateAntiForgeryTokenAttribute),
false)
.Any(),
$"{method.Name} is not decorated with ValidateAntiForgeryToken.");
}
The NUnit TestCaseSource
attribute directs the test runner to execute the test for each element in the enumeration. We
define our test data as every method that is decorated with HttpPost
in all controllers
of the presentation layer assembly.
internal sealed class AllControllerPostMethods : IEnumerable
{
public IEnumerator GetEnumerator()
{
var mvcAssembly = Assembly.Load("NameOfAssemblyContainingControllers");
var controllerTypeInfos = mvcAssembly
.DefinedTypes
.Where(t => t.Name.EndsWith("Controller"));
foreach (var typeInfo in controllerTypeInfos)
{
var methods = mvcAssembly
.GetType(typeInfo.FullName)
.GetRuntimeMethods()
.Where(m => m.GetCustomAttributes(
typeof(HttpPostAttribute), false)
.Any());
foreach (var method in methods)
{
yield return new object[]
{
method
};
}
}
}
}
We load the assembly of the presentation layer under test so that we can get all types that are controllers:
var mvcAssembly = Assembly.Load("NameOfAssemblyContainingControllers");
From this assembly, we can get the TypeInfo
for all types whose names end with
Controller. This works in the case that we have followed controller naming
conventions when creating the classes.
var controllerTypeInfos = mvcAssembly
.DefinedTypes
.Where(t => t.Name.EndsWith("Controller"));
We then iterate over each TypeInfo
representation of the controllers to get all
methods in the controller that are decorated with HttpPost
:
foreach (var typeInfo in controllerTypeInfos)
{
var methods = mvcAssembly
.GetType(typeInfo.FullName)
.GetRuntimeMethods()
.Where(m => m.GetCustomAttributes(
typeof(HttpPostAttribute), false)
.Any());
foreach (var method in methods)
{
yield return new object[]
{
method
};
}
}
If this test fails, we know we have a post method that isn’t configured to validate anti-forgery tokens.