Anti-Forgery Request Helpers for ASP.NET MVC and jQuery AJAX
- by Dixin
Background
To secure websites from cross-site request forgery (CSRF, or XSRF) attack, ASP.NET MVC provides an excellent mechanism:
The server prints tokens to cookie and inside the form;
When the form is submitted to server, token in cookie and token inside the form are sent in the HTTP request;
Server validates the tokens.
To print tokens to browser, just invoke HtmlHelper.AntiForgeryToken():<% using (Html.BeginForm())
{ %>
<%: this.Html.AntiForgeryToken(Constants.AntiForgeryTokenSalt)%>
<%-- Other fields. --%>
<input type="submit" value="Submit" />
<% } %>
This invocation generates a token then writes inside the form:<form action="..." method="post">
<input name="__RequestVerificationToken" type="hidden" value="J56khgCvbE3bVcsCSZkNVuH9Cclm9SSIT/ywruFsXEgmV8CL2eW5C/gGsQUf/YuP" />
<!-- Other fields. -->
<input type="submit" value="Submit" />
</form>
and also writes into the cookie:
__RequestVerificationToken_Lw__= J56khgCvbE3bVcsCSZkNVuH9Cclm9SSIT/ywruFsXEgmV8CL2eW5C/gGsQUf/YuP
When the above form is submitted, they are both sent to server.
In the server side, [ValidateAntiForgeryToken] attribute is used to specify the controllers or actions to validate them:[HttpPost]
[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public ActionResult Action(/* ... */)
{
// ...
}
This is very productive for form scenarios. But recently, when resolving security vulnerabilities for Web products, some problems are encountered.
Specify validation on controller (not on each action)
The server side problem is, It is expected to declare [ValidateAntiForgeryToken] on controller, but actually it has be to declared on each POST actions. Because POST actions are usually much more then controllers, this is a little crazy
Problem
Usually a controller contains actions for HTTP GET and actions for HTTP POST requests, and usually validations are expected for HTTP POST requests. So, if the [ValidateAntiForgeryToken] is declared on the controller, the HTTP GET requests become invalid:[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public class SomeController : Controller // One [ValidateAntiForgeryToken] attribute.
{
[HttpGet]
public ActionResult Index() // Index() cannot work.
{
// ...
}
[HttpPost]
public ActionResult PostAction1(/* ... */)
{
// ...
}
[HttpPost]
public ActionResult PostAction2(/* ... */)
{
// ...
}
// ...
}
If browser sends an HTTP GET request by clicking a link: http://Site/Some/Index, validation definitely fails, because no token is provided.
So the result is, [ValidateAntiForgeryToken] attribute must be distributed to each POST action:public class SomeController : Controller // Many [ValidateAntiForgeryToken] attributes.
{
[HttpGet]
public ActionResult Index() // Works.
{
// ...
}
[HttpPost]
[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public ActionResult PostAction1(/* ... */)
{
// ...
}
[HttpPost]
[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public ActionResult PostAction2(/* ... */)
{
// ...
}
// ...
}
This is a little bit crazy, because one application can have a lot of POST actions.
Solution
To avoid a large number of [ValidateAntiForgeryToken] attributes (one for each POST action), the following ValidateAntiForgeryTokenAttribute wrapper class can be helpful, where HTTP verbs can be specified:[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false, Inherited = true)]
public class ValidateAntiForgeryTokenWrapperAttribute : FilterAttribute, IAuthorizationFilter
{
private readonly ValidateAntiForgeryTokenAttribute _validator;
private readonly AcceptVerbsAttribute _verbs;
public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs)
: this(verbs, null)
{
}
public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs, string salt)
{
this._verbs = new AcceptVerbsAttribute(verbs);
this._validator = new ValidateAntiForgeryTokenAttribute()
{
Salt = salt
};
}
public void OnAuthorization(AuthorizationContext filterContext)
{
string httpMethodOverride = filterContext.HttpContext.Request.GetHttpMethodOverride();
if (this._verbs.Verbs.Contains(httpMethodOverride, StringComparer.OrdinalIgnoreCase))
{
this._validator.OnAuthorization(filterContext);
}
}
}
When this attribute is declared on controller, only HTTP requests with the specified verbs are validated:[ValidateAntiForgeryTokenWrapper(HttpVerbs.Post, Constants.AntiForgeryTokenSalt)]
public class SomeController : Controller
{
// GET actions are not affected.
// Only HTTP POST requests are validated.
}
Now one single attribute on controller turns on validation for all POST actions.
Maybe it would be nice if HTTP verbs can be specified on the built-in [ValidateAntiForgeryToken] attribute, which is easy to implemented.
Submit token via AJAX
The browser side problem is, if server side turns on anti-forgery validation for POST, then AJAX POST requests will fail be default.
Problem
For AJAX scenarios, when request is sent by jQuery instead of form:$.post(url, {
productName: "Tofu",
categoryId: 1 // Token is not posted.
}, callback);
This kind of AJAX POST requests will always be invalid, because server side code cannot see the token in the posted data.
Solution
The tokens are printed to browser then sent back to server. So first of all, HtmlHelper.AntiForgeryToken() must be called somewhere. Now the browser has token in HTML and cookie.
Then jQuery must find the printed token in the HTML, and append token to the data before sending:$.post(url, {
productName: "Tofu",
categoryId: 1,
__RequestVerificationToken: getToken() // Token is posted.
}, callback);
To be reusable, this can be encapsulated into a tiny jQuery plugin:/// <reference path="jquery-1.4.2.js" />
(function ($) {
$.getAntiForgeryToken = function (tokenWindow, appPath) {
// HtmlHelper.AntiForgeryToken() must be invoked to print the token.
tokenWindow = tokenWindow && typeof tokenWindow === typeof window ? tokenWindow : window;
appPath = appPath && typeof appPath === "string" ? "_" + appPath.toString() : "";
// The name attribute is either __RequestVerificationToken,
// or __RequestVerificationToken_{appPath}.
tokenName = "__RequestVerificationToken" + appPath;
// Finds the <input type="hidden" name={tokenName} value="..." /> from the specified.
// var inputElements = $("input[type='hidden'][name='__RequestVerificationToken" + appPath + "']");
var inputElements = tokenWindow.document.getElementsByTagName("input");
for (var i = 0; i < inputElements.length; i++) {
var inputElement = inputElements[i];
if (inputElement.type === "hidden" && inputElement.name === tokenName) {
return {
name: tokenName,
value: inputElement.value
};
}
}
return null;
};
$.appendAntiForgeryToken = function (data, token) {
// Converts data if not already a string.
if (data && typeof data !== "string") {
data = $.param(data);
}
// Gets token from current window by default.
token = token ? token : $.getAntiForgeryToken(); // $.getAntiForgeryToken(window).
data = data ? data + "&" : "";
// If token exists, appends {token.name}={token.value} to data.
return token ? data + encodeURIComponent(token.name) + "=" + encodeURIComponent(token.value) : data;
};
// Wraps $.post(url, data, callback, type).
$.postAntiForgery = function (url, data, callback, type) {
return $.post(url, $.appendAntiForgeryToken(data), callback, type);
};
// Wraps $.ajax(settings).
$.ajaxAntiForgery = function (settings) {
settings.data = $.appendAntiForgeryToken(settings.data);
return $.ajax(settings);
};
})(jQuery);
In most of the scenarios, it is Ok to just replace $.post() invocation with $.postAntiForgery(), and replace $.ajax() with $.ajaxAntiForgery():$.postAntiForgery(url, {
productName: "Tofu",
categoryId: 1
}, callback); // Token is posted.
There might be some scenarios of custom token. Here $.appendAntiForgeryToken() is provided:data = $.appendAntiForgeryToken(data, token);
// Token is already in data. No need to invoke $.postAntiForgery().
$.post(url, data, callback);
And there are scenarios that the token is not in the current window. For example, an HTTP POST request can be sent by iframe, while the token is in the parent window. Here window can be specified for $.getAntiForgeryToken():data = $.appendAntiForgeryToken(data, $.getAntiForgeryToken(window.parent));
// Token is already in data. No need to invoke $.postAntiForgery().
$.post(url, data, callback);
If you have better solution, please do tell me.