阻止 ASP.NET MVC 应用程序中的跨站点请求伪造(CSRF)攻击Preventing Cross-Site Request Forgery (CSRF) Attacks in ASP.NET MVC Application

作者: Mike Wassonby Mike Wasson

跨站点请求伪造(CSRF)是一种攻击,其中恶意站点将请求发送到用户当前登录的有漏洞站点Cross-Site Request Forgery (CSRF) is an attack where a malicious site sends a request to a vulnerable site where the user is currently logged in

下面是 CSRF 攻击的示例:Here is an example of a CSRF attack:

  1. 用户使用窗体身份验证登录 www.example.comA user logs into www.example.com using forms authentication.

  2. 服务器对用户进行身份验证。The server authenticates the user. 来自服务器的响应包含一个身份验证 cookie。The response from the server includes an authentication cookie.

  3. 如果不注销,用户将访问恶意网站。Without logging out, the user visits a malicious web site. 此恶意网站包含以下 HTML 格式:This malicious site contains the following HTML form:

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

    请注意,窗体操作会发布到易受攻击的站点,而不是恶意站点。Notice that the form action posts to the vulnerable site, not to the malicious site. 这是 CSRF 的 "跨站点" 部分。This is the "cross-site" part of CSRF.

  4. 用户单击 "提交" 按钮。The user clicks the submit button. 浏览器包含请求中的身份验证 cookie。The browser includes the authentication cookie with the request.

  5. 请求在服务器上使用用户的身份验证上下文运行,并可执行允许经过身份验证的用户执行的任何操作。The request runs on the server with the user's authentication context, and can do anything that an authenticated user is allowed to do.

尽管此示例要求用户单击 "窗体" 按钮,但恶意页面可以轻松地运行自动提交窗体的脚本。Although this example requires the user to click the form button, the malicious page could just as easily run a script that submits the form automatically. 而且,使用 SSL 不会阻止 CSRF 攻击,因为恶意站点可以发送 "https://" 请求。Moreover, using SSL does not prevent a CSRF attack, because the malicious site can send an "https://" request.

通常,可以对使用 cookie 进行身份验证的网站执行 CSRF 攻击,因为浏览器会将所有相关 cookie 发送到目标网站。Typically, CSRF attacks are possible against web sites that use cookies for authentication, because browsers send all relevant cookies to the destination web site. 但是,CSRF 攻击并不局限于利用 cookie。However, CSRF attacks are not limited to exploiting cookies. 例如,基本身份验证和摘要式身份验证也容易受到攻击。For example, Basic and Digest authentication are also vulnerable. 用户使用基本或摘要式身份验证登录后。After a user logs in with Basic or Digest authentication. 浏览器会自动发送凭据,直到会话结束。the browser automatically sends the credentials until the session ends.

防伪标记Anti-Forgery Tokens

为了帮助防止 CSRF 攻击,ASP.NET MVC 使用了防伪令牌,也称为请求验证令牌To help prevent CSRF attacks, ASP.NET MVC uses anti-forgery tokens, also called request verification tokens.

  1. 客户端请求包含窗体的 HTML 页面。The client requests an HTML page that contains a form.
  2. 服务器在响应中包括两个标记。The server includes two tokens in the response. 一个令牌作为 cookie 发送。One token is sent as a cookie. 另一个被置于隐藏的窗体字段中。The other is placed in a hidden form field. 令牌是随机生成的,因此,攻击者无法猜测值。The tokens are generated randomly so that an adversary cannot guess the values.
  3. 当客户端提交窗体时,它必须将两个令牌都发送回服务器。When the client submits the form, it must send both tokens back to the server. 客户端将 cookie 令牌作为 cookie 发送,并在表单数据内发送窗体令牌。The client sends the cookie token as a cookie, and it sends the form token inside the form data. (当用户提交窗体时,浏览器客户端会自动执行此功能。)(A browser client automatically does this when the user submits the form.)
  4. 如果请求不包含这两个令牌,服务器将不允许该请求。If a request does not include both tokens, the server disallows the request.

下面是带有隐藏的窗体标记的 HTML 窗体的示例:Here is an example of an HTML form with a hidden form token:

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

由于存在相同的源策略,恶意页面无法读取用户的令牌,因此,防伪令牌工作正常。Anti-forgery tokens work because the malicious page cannot read the user's tokens, due to same-origin policies. 相同源策略阻止在两个不同站点上托管的文档访问彼此的内容。(Same-origin policies prevent documents hosted on two different sites from accessing each other's content. 因此,在前面的示例中,恶意页面可以将请求发送到 example.com,但无法读取响应。)So in the earlier example, the malicious page can send requests to example.com, but it cannot read the response.)

若要防止 CSRF 攻击,请将防伪令牌与任何身份验证协议一起使用,浏览器会在用户登录后自动发送凭据。To prevent CSRF attacks, use anti-forgery tokens with any authentication protocol where the browser silently sends credentials after the user logs in. 这包括基于 cookie 的身份验证协议(如 forms 身份验证)以及基本和摘要式身份验证等协议。This includes cookie-based authentication protocols, such as forms authentication, as well as protocols such as Basic and Digest authentication.

对于任何 nonsafe 方法(POST、PUT、DELETE),都应需要防伪令牌。You should require anti-forgery tokens for any nonsafe methods (POST, PUT, DELETE). 此外,请确保安全方法(GET、HEAD)没有任何副作用。Also, make sure that safe methods (GET, HEAD) do not have any side effects. 而且,如果你启用了跨域支持(如 CORS 或 JSONP),则甚至安全方法(如 GET)都可能容易遭受 CSRF 攻击,使得攻击者能够读取可能敏感的数据。Moreover, if you enable cross-domain support, such as CORS or JSONP, then even safe methods like GET are potentially vulnerable to CSRF attacks, allowing the attacker to read potentially sensitive data.

ASP.NET MVC 中的防伪令牌Anti-Forgery Tokens in ASP.NET MVC

若要将防伪令牌添加到 Razor 页面,请使用HtmlHelper. AntiForgeryToken helper 方法:To add the anti-forgery tokens to a Razor page, use the HtmlHelper.AntiForgeryToken helper method:

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

此方法可添加隐藏的窗体字段,还可以设置 cookie 令牌。This method adds the hidden form field and also sets the cookie token.

反 CSRF 和 AJAXAnti-CSRF and AJAX

对于 AJAX 请求,窗体标记可能是一个问题,因为 AJAX 请求可能发送 JSON 数据,而不是 HTML 表单数据。The form token can be a problem for AJAX requests, because an AJAX request might send JSON data, not HTML form data. 一种解决方法是在自定义 HTTP 标头中发送令牌。One solution is to send the tokens in a custom HTTP header. 以下代码使用 Razor 语法生成令牌,然后将令牌添加到 AJAX 请求。The following code uses Razor syntax to generate the tokens, and then adds the tokens to an AJAX request. 令牌是通过调用防伪在服务器上生成的。The tokens are generated at the server by calling 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>

处理请求时,请从请求标头中提取令牌。When you process the request, extract the tokens from the request header. 然后调用防伪方法来验证令牌。Then call the AntiForgery.Validate method to validate the tokens. 如果标记无效,则Validate方法将引发异常。The Validate method throws an exception if the tokens are not valid.

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);
}