防止 ASP.NET MVC 应用程序中的跨站点请求伪造 (CSRF) 攻击

跨站点请求伪造 (CSRF) 是恶意站点将请求发送到用户当前登录的有漏洞站点的攻击

下面是 CSRF 攻击的示例:

  1. 用户使用 Forms 身份验证登录 www.example.com

  2. 服务器对用户进行身份验证。 来自服务器的响应包括身份验证 Cookie。

  3. 如果不注销,用户将访问恶意网站。 此恶意网站包含以下 HTML 表单:

    <h1>You Are a Winner!</h1>
      <form action="http://example.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
      <input type="submit" value="Click Me"/>
    </form>
    

    请注意,表单操作发布到易受攻击的网站,而不是恶意站点。 这是 CSRF 的“跨网站”部分。

  4. 用户单击“提交”按钮。 浏览器将身份验证 Cookie 与请求一起包含在内。

  5. 请求使用用户的身份验证上下文在服务器上运行,并且可以执行经过身份验证的用户允许执行的任何操作。

尽管此示例要求用户单击窗体按钮,但恶意页面同样可以轻松运行自动提交表单的脚本。 此外,使用 SSL 不会阻止 CSRF 攻击,因为恶意站点可能会发送“https://”请求。

通常,CSRF 攻击可能会针对使用 Cookie 进行身份验证的网站,因为浏览器会将所有相关 Cookie 发送到目标网站。 但是,CSRF 攻击并不局限于利用 Cookie。 例如,基本身份验证和摘要式身份验证也容易受到攻击。 用户使用基本或摘要式身份验证登录后。 浏览器会自动发送凭据,直到会话结束。

防伪令牌

为了帮助防止 CSRF 攻击,ASP.NET MVC 使用防伪令牌,也称为 请求验证令牌

  1. 客户端请求包含表单的 HTML 页面。
  2. 服务器在响应中包含两个令牌。 一个令牌作为 Cookie 发送。 另一个放置在隐藏的窗体字段中。 令牌是随机生成的,因此攻击者无法猜测这些值。
  3. 当客户端提交表单时,它必须将这两个令牌发送回服务器。 客户端将 Cookie 令牌作为 Cookie 发送,并在表单数据中发送表单令牌。 (当用户提交 form.)
  4. 如果请求不包含这两个令牌,则服务器将禁止该请求。

下面是带有隐藏窗体标记的 HTML 窗体的示例:

<form action="/Home/Test" method="post">
    <input name="__RequestVerificationToken" type="hidden"   
           value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" />    
    <input type="submit" value="Submit" />
</form>

防伪令牌之所以有效,是因为恶意页面由于同源策略而无法读取用户的令牌。 (同源策略 阻止托管在两个不同站点上的文档访问彼此的内容。因此在前面的示例中,恶意页面可以向 example.com 发送请求,但无法读取 response。)

若要防止 CSRF 攻击,请将防伪令牌与任何身份验证协议结合使用,浏览器会在用户登录后以无提示方式发送凭据。 这包括基于 Cookie 的身份验证协议(例如表单身份验证)以及基本和摘要式身份验证等协议。

对于任何不安全的方法,应要求使用防伪令牌, (POST、PUT、DELETE) 。 此外,请确保安全方法 (GET,HEAD) 没有任何副作用。 此外,如果启用跨域支持(如 CORS 或 JSONP),则即使是 GET 等安全方法也可能容易受到 CSRF 攻击,从而允许攻击者读取潜在的敏感数据。

ASP.NET MVC 中的防伪令牌

若要将防伪令牌添加到 Razor 页面,请使用 HtmlHelper.AntiForgeryToken 帮助程序方法:

@using (Html.BeginForm("Manage", "Account")) {
    @Html.AntiForgeryToken()
}

此方法添加隐藏的窗体字段并设置 Cookie 标记。

反 CSRF 和 AJAX

表单令牌可能是 AJAX 请求的问题,因为 AJAX 请求可能会发送 JSON 数据,而不是 HTML 表单数据。 一种解决方法是在自定义 HTTP 标头中发送令牌。 以下代码使用 Razor 语法生成令牌,然后将令牌添加到 AJAX 请求。 令牌通过调用 AntiForgery.GetTokens 在服务器上生成。

<script>
    @functions{
        public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;                
        }
    }

    $.ajax("api/values", {
        type: "post",
        contentType: "application/json",
        data: {  }, // JSON data goes here
        dataType: "json",
        headers: {
            'RequestVerificationToken': '@TokenHeaderValue()'
        }
    });
</script>

处理请求时,请从请求标头中提取令牌。 然后调用 AntiForgery.Validate 方法来验证令牌。 如果令牌无效, Validate 方法将引发异常。

void ValidateRequestHeader(HttpRequestMessage request)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
    {
        string[] tokens = tokenHeaders.First().Split(':');
        if (tokens.Length == 2)
        {
            cookieToken = tokens[0].Trim();
            formToken = tokens[1].Trim();
        }
    }
    AntiForgery.Validate(cookieToken, formToken);
}