防止 ASP.NET MVC 应用程序中的跨站点请求伪造 (CSRF) 攻击
跨站点请求伪造 (CSRF) 是恶意站点将请求发送到用户当前登录的有漏洞站点的攻击
下面是 CSRF 攻击的示例:
用户使用 Forms 身份验证登录
www.example.com
。服务器对用户进行身份验证。 来自服务器的响应包括身份验证 Cookie。
如果不注销,用户将访问恶意网站。 此恶意网站包含以下 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 的“跨网站”部分。
用户单击“提交”按钮。 浏览器将身份验证 Cookie 与请求一起包含在内。
请求使用用户的身份验证上下文在服务器上运行,并且可以执行经过身份验证的用户允许执行的任何操作。
尽管此示例要求用户单击窗体按钮,但恶意页面同样可以轻松运行自动提交表单的脚本。 此外,使用 SSL 不会阻止 CSRF 攻击,因为恶意站点可能会发送“https://”请求。
通常,CSRF 攻击可能会针对使用 Cookie 进行身份验证的网站,因为浏览器会将所有相关 Cookie 发送到目标网站。 但是,CSRF 攻击并不局限于利用 Cookie。 例如,基本身份验证和摘要式身份验证也容易受到攻击。 用户使用基本或摘要式身份验证登录后。 浏览器会自动发送凭据,直到会话结束。
防伪令牌
为了帮助防止 CSRF 攻击,ASP.NET MVC 使用防伪令牌,也称为 请求验证令牌。
- 客户端请求包含表单的 HTML 页面。
- 服务器在响应中包含两个令牌。 一个令牌作为 Cookie 发送。 另一个放置在隐藏的窗体字段中。 令牌是随机生成的,因此攻击者无法猜测这些值。
- 当客户端提交表单时,它必须将这两个令牌发送回服务器。 客户端将 Cookie 令牌作为 Cookie 发送,并在表单数据中发送表单令牌。 (当用户提交 form.)
- 如果请求不包含这两个令牌,则服务器将禁止该请求。
下面是带有隐藏窗体标记的 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);
}