Background (Normal scenario of form submitting)

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 and writes it 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 it 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, problems are encountered.

Turn on validation on controller (not on each action)

The server side problem is, one single [ValidateAntiForgeryToken] attribute is expected to declare on controller, but actually a lot of attributes have be to declared on controller's each POST actions. Because POST actions are usually much more then controllers, the work would be a little crazy.

Problem

Usually a controller contains both actions for HTTP GET requests and actions for POST, and, usually validations are expected for only HTTP POST requests. So, if the [ValidateAntiForgeryToken] is declared on the controller, the HTTP GET requests become invalid:

[ValidateAntiForgeryToken(Salt = Constants.AntiForgeryTokenSalt)]
public class ProductController : Controller // One [ValidateAntiForgeryToken] attribute.
{
[HttpGet]
public ActionResult Index() // Index() cannot work.
{
// ...
} [HttpPost]
public ActionResult PostAction1(/* ... */)
{
// ...
} [HttpPost]
public ActionResult PostAction2(/* ... */)
{
// ...
} // Other actions.
}

If browser sends an HTTP GET request by clicking a link: http://Site/Product/Index, validation definitely fails, because no token is provided (by http://Site/Product/Index?__RequestVerificationToken=???, for example).

As a result, many [ValidateAntiForgeryToken] attributes have be distributed to each POST action:

public class ProductController : 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(/* ... */)
{
// ...
} // Other actions.
}

This would be a little bit crazy, because one Web product can have a lot of POST actions.

Solution

To avoid a large number of [ValidateAntiForgeryToken] attributes (one for each POST action), the following ValidateAntiForgeryTokenWrapperAttribute 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);
}
}
}

Here only HTTP requests of the specified verbs are validated:

[ValidateAntiForgeryTokenWrapper(HttpVerbs.Post, Constants.AntiForgeryTokenSalt)]
public class ProductController : Controller
{
// GET actions are not affected.
// Only HTTP POST requests are validated.
}

Now one single attribute on a controller turns on validation for all POST actions in that controller.

It would be nice if HTTP verbs can be specified on the built-in [ValidateAntiForgeryToken] attribute. And, this is very easy to implement.

Specify non-constant salt in runtime

By default, the salt should be a compile time constant, so it can be used for the [ValidateAntiForgeryToken] or [ValidateAntiForgeryTokenWrapper] attribute.

Problem

One Web product might be sold to many clients. If a constant salt is evaluated in compile time, after the product is built and deployed to many clients, they all have the same salt. Of course, clients do not like this. Even some clients might expect a configurable custom salt. In these scenarios, salt is required to be a runtime value.

Solution

In the above [ValidateAntiForgeryToken] and [ValidateAntiForgeryTokenWrapper] attributes, the salt is passed through constructor. So one solution is to remove that parameter:

public class ValidateAntiForgeryTokenWrapperAttribute : FilterAttribute, IAuthorizationFilter
{
public ValidateAntiForgeryTokenWrapperAttribute(HttpVerbs verbs)
{
this._verbs = new AcceptVerbsAttribute(verbs);
this._validator = new ValidateAntiForgeryTokenAttribute()
{
Salt = Configurations.AntiForgeryTokenSalt
};
} // Other members.
}

But this smells bad because the injected dependency becomes a hard dependency. So the other solution to work around the limitation of attributes, is moving validation code into controller:

public abstract class AntiForgeryControllerBase : Controller
{
private readonly ValidateAntiForgeryTokenAttribute _validator; private readonly AcceptVerbsAttribute _verbs; protected AntiForgeryControllerBase(HttpVerbs verbs, string salt)
{
this._verbs = new AcceptVerbsAttribute(verbs);
this._validator = new ValidateAntiForgeryTokenAttribute()
{
Salt = salt
};
} protected override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext); string httpMethodOverride = filterContext.HttpContext.Request.GetHttpMethodOverride();
if (this._verbs.Verbs.Contains(httpMethodOverride, StringComparer.OrdinalIgnoreCase))
{
this._validator.OnAuthorization(filterContext);
}
}
}

Then just make controller classes inheriting from this AntiForgeryControllerBase class. Now the salt is no long required to be a compile time constant.

Submit token via AJAX

For browser side, once server side turns on anti-forgery validation for HTTP POST, all AJAX POST requests will fail by default.

Problem

In AJAX scenarios, the HTTP POST request is not sent by form. Take jQuery as an example:

$.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

Basically, the tokens must be printed to browser then sent back to server. So first of all, HtmlHelper.AntiForgeryToken() need to be called somewhere. Now the browser has token in both 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}.
var tokenName = "__RequestVerificationToken" + appPath; // Finds the <input type="hidden" name={tokenName} value="..." /> from the specified window.
// var inputElements = tokenWindow.$("input[type='hidden'][name=' + tokenName + "']");
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
};
}
}
}; $.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) for most common scenarios.
$.postAntiForgery = function (url, data, callback, type) {
return $.post(url, $.appendAntiForgeryToken(data), callback, type);
}; // Wraps $.ajax(settings).
$.ajaxAntiForgery = function (settings) {
// Supports more options than $.ajax():
// settings.token, settings.tokenWindow, settings.appPath.
var token = settings.token ? settings.token : $.getAntiForgeryToken(settings.tokenWindow, settings.appPath);
settings.data = $.appendAntiForgeryToken(settings.data, token);
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); // The same usage as $.post(), but token is posted.

There might be some scenarios of custom token, where $.appendAntiForgeryToken() is useful:

data = $.appendAntiForgeryToken(data, token);
// Token is already in data. No need to invoke $.postAntiForgery().
$.post(url, data, callback);

or $.ajaxAntiForgery() can be used:

$.ajaxAntiForgery({
type: "POST",
url: url,
data: {
productName: "Tofu",
categoryId: 1
},
success: callback, // The same usage as $.ajax(), supporting more options.
token: token // Custom token.
});

And there are special scenarios that the token is not in the current window. For example:

  • An HTTP POST request can be sent from an iframe, while the token is in the parent window or top window;
  • An HTTP POST request can be sent from an popup window or a dialog, while the token is in the opener window;

etc. Here, token's container 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);

or $.ajaxAntiForgery() can be used:

$.ajaxAntiForgery({
type: "POST",
url: url,
data: {
productName: "Tofu",
categoryId: 1
},
success: callback, // The same usage as $.ajax(), supporting more options.
tokenWindow: window.parent // Token is in another window.
});

If you have better solution, please do tell me.

Anti-Forgery Request Recipes For ASP.NET MVC And AJAX的更多相关文章

  1. ASP.NET MVC 实现AJAX跨域请求方法《1》

    ASP.NET MVC 实现AJAX跨域请求的两种方法 通常发送AJAX请求都是在本域内完成的,也就是向本域内的某个URL发送请求,完成部分页面的刷新.但有的时候需要向其它域发送AJAX请求,完成数据 ...

  2. [代码示例]用Fine Uploader+ASP.NET MVC实现ajax文件上传

    原文 [代码示例]用Fine Uploader+ASP.NET MVC实现ajax文件上传 Fine Uploader(http://fineuploader.com/)是一个实现 ajax 上传文件 ...

  3. Asp.Net MVC 使用 Ajax

    Asp.Net MVC 使用 Ajax Ajax 简单来说Ajax是一个无需重新加载整个网页的情况下,可以更新局部页面或数据的技术(异步的发送接收数据,不会干扰当前页面). Ajax工作原理 Ajax ...

  4. ASP.NET MVC之Ajax如影随行

    一.Ajax的前世今生 我一直觉得google是一家牛逼的公司,为什么这样说呢?<舌尖上的中国>大家都看了,那些美食估计你是百看不厌,但是里边我觉得其实也有这样的一个哲学:关于食材,对于种 ...

  5. ASP.NET MVC 实现 AJAX 跨域请求

    ASP.NET MVC 实现AJAX跨域请求的两种方法 和大家分享下Ajax 跨域的经验,之前也找了好多资料,但是都不行,后来看到个可行的修改了并测试下 果然OK了   希望对大家有所帮助! 通常发送 ...

  6. asp.net mvc 使用ajax请求 控制器 (PartialViewResult)分部的action,得到一个分部视图(PartialView)的HTML,进行渲染

    在asp.net mvc 使用ajax请求获取数据的时候,我们一般是返回json或者xml,然后解析这些数据进行渲染,这样会比较麻烦,可以请求一个 分部action,返回一个分部视图 直接可以渲染,不 ...

  7. 在Asp.Net MVC中用Ajax回调后台方法

    在Asp.Net MVC中用Ajax回调后台方法基本格式: var operData = ...; //传递的参数(action中定义的) var type = ...; //传递的参数(action ...

  8. Asp.Net MVC Unobtrusive Ajax

    1.   Unobtrusive JavaScript介绍 说到Unobtrusive Ajax,就要谈谈UnobtrusiveJavaScript了,所谓Unobtrusive JavaScript ...

  9. ASP.NET MVC 学习笔记-7.自定义配置信息 ASP.NET MVC 学习笔记-6.异步控制器 ASP.NET MVC 学习笔记-5.Controller与View的数据传递 ASP.NET MVC 学习笔记-4.ASP.NET MVC中Ajax的应用 ASP.NET MVC 学习笔记-3.面向对象设计原则

    ASP.NET MVC 学习笔记-7.自定义配置信息   ASP.NET程序中的web.config文件中,在appSettings这个配置节中能够保存一些配置,比如, 1 <appSettin ...

随机推荐

  1. disconf-自动注入属性变化

    disconf-自动注入属性变化 disconf官网:http://disconf.readthedocs.io/zh_CN/latest/ 自动注入属性:http://disconf.readthe ...

  2. MySQL表级锁和行级锁

    一:概述 相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制.比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking ...

  3. C# 源码 AForge.NET

    AForge.NET是一个专门为开发者和研究者基于C#框架设计的,他包括计算机视觉与人工智能,图像处理,神经网络,遗传算法,机器学习,模糊系统,机器人控制等领域.这个框架由一系列的类库组成.主要包括有 ...

  4. MVC 之 缓存机制(二)

    八.应用程序缓存 应用程序缓存提供了一种编程方式,可通过键/值对将任意数据存储在内存中. 使用应用程序缓存与使用应用程序状态类似. 但是,与应用程序状态不同的是,应用程序缓存中的数据是易失的, 即数据 ...

  5. fread/fwrite

    fread/fwrite第二个参数和第三个参数的区别以及与返回值的关系 size_t fwrite_unlocked(const void * __restrict ptr, size_t size, ...

  6. bash if 表达式含义

    [ -a FILE ] 如果 FILE 存在则为真. [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真. [ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真. [ ...

  7. Python中字符串的Format用法。

    一.例子: "]) "_".join(map(lambda x:str(x),[1,2,3,4])) "{0}-{1}".format(3.4,34) ...

  8. 转 VB ListView控件各种操作详解

    Private Sub Form_Load() ListView1.ListItems.Clear '清空列表 ListView1.ColumnHeaders.Clear '清空列表头 ListVie ...

  9. Android玩转百度地图Sha1获取正确姿势?

    场景一 由于近期项目钟要用到定位功能因此肯定须要用到地图以及地位功能,相信大家也知道眼下国内比較出名的地图像百度.高德.腾讯等这些还是用到比較多的.于是思考了一下决定还是用百度,相信老司机们都知道的哈 ...

  10. Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理

    Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理 2017年01月04日 08:52:12 阅读数:18366 基于Lucene检索引擎我们开发了自己的全文检索系统,承担起后台PB ...