CSRF、@Html.AntiForgeryToken()、[ValidateAntiForgeryToken]

创建时间:
2018-08-24 20:36
最近更新:
2018-09-04 23:20

Keywords

  • CSRF (Cross Site Request Forgery, 跨站域请求伪造)
  • antiforgery
  • AntiForgery
  • @Html.AntiForgeryToken()
  • [ValidateAntiForgeryToken]
  • ValidateAntiForgeryTokenAttribute

Brief

  • 定义一: CSRF 攻击 可以在受害者毫不知情的情况下 以受害者的名义 伪造请求 发送给 受攻击站点,从而 在并未授权的情况下 执行在权限保护之下的操作。
  • 定义二: CSRF 是指 利用受害者尚未失效的身份认证信息 (cookie, session etc.),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向 (身份认证信息所对应的) 服务器发送请求,从而完成非法操作 (如转账、改密等)。CSRF 与 XSS 最大的区别就在于,CSRF 并没有盗取 cookie 而是直接利用。
  • 网摘: Html.AntiForgeryToken() 会生成一对加密的字符串,分别存放在 cookie 和 input 中。

CSRF 攻击实例 - GET

假设某网站被设计为 通过 get 请求以下 URL 即可修改当前用户登录用户的 Email:

http://a.com/User/ChangeEmail/NewEmailOfUser0@example.com

那么,攻击者想办法让 a.com 的用户在攻击者的网站上触发对以下 URL 的 get 请求:

http://a.com/User/ChangeEmail/Attacker@example.com

如果该用户的 cookie/session 未过期,那么 该用户的邮箱 被成功修改为 攻击者的邮箱。

注: 发起 get 请求的方式有 <img src=""><a href=""> 等。

CSRF 攻击实例、CSRF 攻击的对象、生成 token 的方法

以下内容摘自 CSRF 攻击的应对之道 by 牛刚 和 童强国 on 2011-02-24

CSRF 攻击实例

受害者 Bob 在银行有一笔存款,通过对银行的网站发送请求

http://bank.example/withdraw?account=Bob&amount=1000000&for=Bob2

可以使 Bob 把 1000000 的存款转到 Bob2 的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 Session,并且该 Session 的用户 Bob 已经成功登陆。

黑客 Mallory 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。Mallory 可以自己发送一个请求给银行:

http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory

但是这个请求来自 Mallory 而非 Bob,他不能通过安全认证,因此该请求不会起作用。

这时,Mallory 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码:

src="
http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory
"

并且通过广告等诱使 Bob 来访问他的网站。当 Bob 访问该网站时,上述 URL 就会从 Bob 的浏览器发向银行,而这个请求会附带 Bob 浏览器中的 Cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 Bob 的认证信息。但是,如果 Bob 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 Session 尚未过期,浏览器的 Cookie 之中含有 Bob 的认证信息。这时,悲剧发生了,这个 URL 请求就会得到响应,钱将从 Bob 的账号转移到 Mallory 的账号,而 Bob 当时毫不知情。等以后 Bob 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 Mallory 则可以拿到钱后逍遥法外。

CSRF 攻击的对象

在讨论如何抵御 CSRF 之前,先要明确 CSRF 攻击的对象,也就是要保护的对象。从以上的例子可知,CSRF 攻击是黑客借助受害者的 Cookie 骗取服务器的信任,但是黑客并不能拿到 Cookie,也看不到 Cookie 的内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客也无法进行解析。因此,黑客无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。所以,我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行 CSRF 的保护。比如银行系统中转账的请求会直接改变账户的金额,会遭到 CSRF 攻击,需要保护。而查询余额是对金额的读取操作,不会改变数据,CSRF 攻击无法解析服务器返回的结果,无需保护。

生成 token 的方法

生成 token 有很多种方法,任何的随机算法都可以使用,Java 的 UUID 类也是一个不错的选择。

@Html.AntiForgeryToken()[ValidateAntiForgeryToken]

  • get 请求时 [ValidateAntiForgeryToken] 会抛异常。详见下方测试。
  • @Html.AntiForgeryToken()[ValidateAntiForgeryToken] 必须同时使用。如果 有前者 无后者 则 不进行验证; 如果 无前者 有后者 则 运行时抛出异常。详见下方测试。

@Html.AntiForgeryToken()[ValidateAntiForgeryToken] 测试

一、post

测试代码

Controller:

using System;
using System.Web;
using System.Web.Mvc;

namespace Site.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Test(string __RequestVerificationToken)
        {
            HttpCookieCollection cookies = Request.Cookies;
            string cookieVal = cookies[0].Value;
            return Content(String.Join(
                "\r\n",
                "<pre>",
                "by post:",
                (cookies.AllKeys.Length == 1) + " //只有一个 cookie",
                (cookies.GetKey(0) == "__RequestVerificationToken") + " //cookie 与 input 的 name 相同",
                (cookieVal != __RequestVerificationToken) + " //cookie 与 input 的 value 不同",
                (cookieVal.Length == __RequestVerificationToken.Length) + " //cookie 与 input 的 value 的 length 相同",
                "input 的 value:",
                __RequestVerificationToken,
                "cookie 的 value:",
                cookieVal,
                "</pre>"
            ));
        }

    }
}

View:

<form action="/Home/Test" method="post">
    @Html.AntiForgeryToken()
    <input type="submit" />
</form>

View 的某次输出:

<form action="/Home/Test" method="post">
    <input name="__RequestVerificationToken" type="hidden" value="N1o0hcpGMCwh8U6pmaaTHQr8ik_rofPgVFjljF8GiaHfVi8GWwIBqAnf3AnNjKq19OZlOWTbUmE7q51b7CQRftxOrvmCokzP1MxKhaxsnbk1">
    <input type="submit">
</form>

Action 的某次输出:

<pre>
by post:
True //只有一个 cookie
True //cookie 与 input 的 name 相同
True //cookie 与 input 的 value 不同
True //cookie 与 input 的 value 的 length 相同
input 的 value:
VAO-dFvPTo1iyKefeVicrHuq4OnFY1ymfCo8PUW3Y3buHx9joTJ2fsLpDTX_xxgRnzK3BicAPwrE5CkkLImwkC3JYb9oq9dJn3h1TqYU5J41
cookie 的 value:
p7cFUNcHuuSZm0ZU85XDv5yvekEEM_aDiFQMOgNWc7lsvLyTIaUPgzbytaqH-KAkGriK126AM_HYygY-qF7EF2GGYFqQUDU-nTAni-U4yQM1
</pre>

测试结果

  • inputcookiename 相同、value 不同,但 valueLength 相同。
  • 页面上 一个或多个 @Html.AntiForgeryToken() 都只生成 一个 cookie。
  • 该 cookie: Name = __RequestVerificationToken, Value = ... , Domain = localhost, Path = /, Expires / Max-Age = Session, Size = 134, HTTP = true。
  • 注意,上一行 Size = 134,而 inputname 的长度为 26,value 的长度为 108,两项的长度相加也是 134。
  • 该 cookie: HttpOnlytrue,因此 JavaScript 无法获取。
  • 每次刷新浏览器 该 cookie 的值不变。
  • 同一个 form 中可以有一个或者多个 @Html.AntiForgeryToken(),提交后 模型绑定 会将第一个 inputvalue 赋给 Action 的参数 string __RequestVerificationToken
  • 每次刷新浏览器 @Html.AntiForgeryToken() 生成的 inputvalue 都不同。
  • 如果 View 中无 @Html.AntiForgeryToken() 但 Action 上有 [ValidateAntiForgeryToken],服务器收到请求将抛出 System.Web.Mvc.HttpAntiForgeryException,该异常的 Message 为 "所需的防伪表单字段“__RequestVerificationToken”不存在。"
  • 没有 session,当然 也没有 SessionID 的 cookie。

二、get

测试代码

Controller:

using System.Web.Mvc;

namespace Site.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [ValidateAntiForgeryToken]
        public ActionResult Test()
        {
            return Content("by get");
        }

    }
}

View:

<form action="/Home/Test" method="get">
    @Html.AntiForgeryToken()
    <input type="submit" />
</form>

测试结果

服务器收到请求将抛出 System.Web.Mvc.HttpAntiForgeryException,该异常的 Message 为 "所需的防伪表单字段“__RequestVerificationToken”不存在。"。

MVC5 Scaffold (脚手架 / 基架) 自动生成的控制器与视图代码 中的全部相关代码

Controller 中有以下代码:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="PropertyId,PropertyName,ColumnName,IsAllowNull,IsKey,ObjecxId,SqlServerTypeId")] Property property) { }

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="PropertyId,PropertyName,ColumnName,IsAllowNull,IsKey,ObjecxId,SqlServerTypeId")] Property property) { }

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id) { }

Create.cshtml、Edit.cshtml、Delete.cshtml 中都有以下代码:

@using (Html.BeginForm()) {
    ...
    @Html.AntiForgeryToken()
    ...
}

Resource - MSDN

  1. 在 ASP.NET MVC 和 Web Pages 中的 XSRF/CSRF 防护

Resource

  1. 初探 CSRF 在 ASP.NET Core 中的处理方式 - 源码分析
  2. CSRF 攻击的应对之道 by 牛刚 和 童强国 on 2011-02-24
  3. CSRF 漏洞详细说明
  4. 看见 CSRF 我不怕不怕了
  5. CSRF 攻击 及 MVC 中的解决方案
  6. Cross-Site Request Forgery (CSRF)
  7. 浅谈 CSRF 攻击方式
  8. 记得 AJAX 中要带上 AntiForgeryToken 防止 CSRF 攻击