CSRF and AntiForgeryToken
CSRF and AntiForgeryToken
Cross Site Request Forgery also known as CSRF (XSRF) is a widely exploited website vulnerability. In a CSRF attack, a malicious site instructs a victim's browser to send a request to an honest site, as if request were part of the victim's interaction with the honest site, leveraging the victim's network connectivity and the browser's state, such as cookies, to disrupt the integrity of the victim's session with the honest site. One of the popular technique to prevent CSRF attack is by using security tokens (from here).
ASP.NET MVC suports prevention against CSRF through the AntiForgeryToken html helper and ValidateAntiForgeryToken filter. The AntiForgeryToken is supported only for the POST requests and not for GET and this makes sense because the GET operation has to used only for safe operations (as per HTTP spec.).
In some applications we need all the POST operations should be validated for the anti-forgery token and in those cases instead of decorating all the POST actions in the application with the ValidateAntiForgeryTokenAttribute we can create a custom authorization filter and apply it globally, that's what we are going to see in this article. We will also see how to create a html helper that renders form along with the hidden field that contains security token.
What the AntiForgeryToken helper does?
The Html.AntiForgeryToken() creates a security token and sets it to a hidden field along with that it also sets the token to a httponly cookie.
@using(Html.BeginForm()) { <p> @Html.AntiForgeryToken() </p> }
Hidden field
<form ..> ... <input type="hidden" name="__RequestVerificationToken"> value="d1Hh28W3uTpdZcEG0VhEkYg7D5XqFM9Sm4iA2e/cXgrOIIpPDENi1lVBg6mYLBAAoGk0q5RA/EPE2o6W5VAqdziURtyqcdFrEcDmSID4vtOF+Nm2Zgf5EJRoRCTUzWvEgcffCvWgfATcznKjnZExjGcbMYQKLhkWBKydzzi4/UE=" </form>
Cookie
__RequestVerificationToken_Lw__=+9UmEtsg2JLwoNO8eumR6fXWw/LnWU61oOiGvT+wNWPCrzfGSP++ODSJbUaeq7/7sDBjW8vQq3QRpEIRERWGcTFVQUUNVfFx5FM4P8oy0V6vPUNayyn8QM3vCdgYx/WwumyKauQWJb+Ysduni9T2rbKjRXJ6LqLbIcTX8CuOU2g=
What is the use of ValidateAntiForgeryToken(Attribute) filter?
The ValidateAntiForgeryToken filter checks whether the request contains the security token or not and if it doesn't contains the token then it generates the following error.
A required anti-forgery token was not supplied or was invalid.
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Save(Model model) { ... }
One disadvantage is, we can't apply this filter at global level because it check all the requests(GET, POST) for the security token and our requirement is to check only for POST requests. The ValidateAntiForgeryTokenAttribute is a sealed class so we can't extend it through inheritance and we have to go for composition.
Custom AntiForgeryToken filter
public class AntiForgeryAttribute: IAuthorizationFilter { public void OnAuthorization(AuthorizationContext authorizationContext) { if (authorizationContext.RequestContext.HttpContext.Request.HttpMethod != "POST") return; new ValidateAntiForgeryTokenAttribute().OnAuthorization(authorizationContext); } }
The implementation is quite simple! our custom filter checks for the POST requests and if yes then instantiates and calls the OnAuthorization method of the built-in ValidateAntiForgeryToken filter.
Now all we have to do is add our custom AntiForgeryAttribute as global filter in Global.asax.cs.
filters.Add(new AntiForgeryAttribute());
Creating a secure html form
Usually we create a html form that is secured by antiforgery hidden field as below.
@using(Html.BeginForm()) { ... Html.AntiForgeryToken() }
It would be nice if we create a html helper that takes care of rendering the security token hidden field as well as the cookie.
Here is our simple helper.
public static MvcForm BeginSecureForm(this HtmlHelper htmlHelper, string actionName, string controllerName) { TagBuilder tagBuilder = new TagBuilder("form"); tagBuilder.MergeAttribute("action", UrlHelper.GenerateUrl(null, actionName, controllerName, new RouteValueDictionary(), htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, true)); tagBuilder.MergeAttribute("method", "POST", true); htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); htmlHelper.ViewContext.Writer.Write(htmlHelper.AntiForgeryToken().ToHtmlString()); var theForm = new MvcForm(htmlHelper.ViewContext); return theForm; }
Now we can create a secure form simply like this,
@using(Html.SecureForm()) { ... }
By-passing AntiForgeryValidation for some actions
For some POST actions we may don't need the anti-forgery check and with the current implementation we can't achieve that. To satisfy this, all we have to do is create a simple attribute and decorate the actions that doesn't need security check with that.
public class NoAntiForgeryCheckAttribute: Attribute { } [NoAntiForgeryCheck] public ActionResult NotSecuredAction(Model model) { }
We have to update our AntiForgeryAttribute to by-pass those actions decorated with NoAntiForgeryCheckAttribute.
public class AntiForgeryAttribute: IAuthorizationFilter { public void OnAuthorization(AuthorizationContext authorizationContext) { if (authorizationContext.RequestContext.HttpContext.Request.HttpMethod != "POST") return; if (authorizationContext.ActionDescriptor.GetCustomAttributes(typeof(NoAntiForgeryCheckAttribute), true).Length > 0) return; new ValidateAntiForgeryTokenAttribute().OnAuthorization(authorizationContext); } }
Yeah, we are done! Now our POST actions are safe against CSRF vulnerability. Hope so ;)