跨站点请求伪造(CSRF)是一种恶意站点向用户当前登录的易受攻击的站点发送请求的攻击方式。
以下是CSRF攻击的示例(必须具备的条件):
- 用户使用表单验证登录 www.example.com。
- 服务器验证用户,响应(Response)包含了一个认证cookie。
-
用户没有注销,然后访问了恶意网站。此恶意网站包含以下HTML表单:
1 <h1>You Are a Winner!</h1> 2 <form action="http://example.com/api/account" method="post"> 3 <input type="hidden" name="Transaction" value="withdraw" /> 4 <input type="hidden" name="Amount" value="1000000" /> 5 <input type="submit" value="Click Me"/> 6 </form>
请注意,表单的Action是Post到易受攻击的网站,而不是恶意网站。这是CSRF的“跨站点”部分。
- 用户点击提交按钮。浏览器请求包含身份验证cookie。
- 请求在具有用户身份验证上下文的服务器上运行,并且可以做任何经过身份验证的用户才能被允许做的操作。
虽然此示例需要用户主动单击表单按钮,但是恶意页面可以轻松运行自动提交表单的脚本。此外,使用SSL不能阻止CSRF攻击,因为恶意站点也可以发送“https://”请求。
通常,针对使用Cookie进行身份验证的网站可能会发生CSRF攻击,因为浏览器会将所有相关的Cookie发送到目标网站。然而,CSRF攻击并不仅限于利用Cookie。例如,Basic和Digest身份验证也很脆弱。用户使用“Basic”或“Digest”身份验证登录后,浏览器会自动发送凭证直到会话结束。
防伪令牌(Anti-Forgery Tokens)
为了帮助防止CSRF攻击,ASP.NET MVC使用防伪令牌(Anti-Forgery Tokens),也称为请求验证令牌。
- 客户端请求包含表单的HTML页面。
- 服务器在响应中包含两个令牌。一个令牌作为一个Cookie发送。另一个放置在隐藏的表单字段中。令牌随机生成,使攻击者无法猜测令牌值。
- 当客户端提交表单时,它必须将两个令牌发送回服务器。客户端将Cookie令牌作为cookie发送,并在表单数据中发送表单令牌。(浏览器客户端在用户提交表单时自动执行此操作。)
- 如果请求不包含这两个令牌,则服务器不允许该请求。
以下是具有隐藏表单令牌的HTML表单的示例:
<form action="/Home/Test" method="post">
<input name="__RequestVerificationToken" type="hidden"
value="6fGBtLZmVBZ59oUad1Fr33BuPxANKY9q3Srr5y[...]" />
<input type="submit" value="Submit" />
</form>
防伪令牌的工作原理是恶意页面无法读取用户的令牌,因为有同源策略。(同源策略可以防止两个不同站点上托管的文件访问对方的内容,所以在前面的例子中,恶意页面可以发送请求到example.com,但不能读取响应。)
为了防止CSRF攻击,任何在用户登录后浏览器默认发送凭据的身份验证协议都应使用防伪令牌。包括基于cookie的身份验证协议,如表单身份验证以及Basic和 Digest(摘要)身份验证等协议。
您应该对任何非安全方法(POST,PUT,DELETE)要求防伪令牌。另外,确保安全的方法(GET,HEAD)没有任何副作用。此外,如果您启用跨域支持(如CORS或JSONP),则甚至安全的方法(如GET)也可能容易受到CSRF攻击的攻击,从而允许攻击者读取潜在的敏感数据。
ASP.NET MVC中的防伪令牌
要将防伪令牌添加到Razor页面,请使用HtmlHelper.AntiForgeryToken帮助方法:
@using (Html.BeginForm("Manage", "Account")) {
@Html.AntiForgeryToken()
}
此方法添加隐藏表单字段,并设置cookie令牌。
Anti-CSRF 和 AJAX
表单令牌可能是AJAX请求的一个问题,因为AJAX请求可能会发送JSON数据,而不是HTML表单数据。一个解决方案是将Token发送到自定义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方法来验证令牌。如果令牌无效,该验证方法将引发异常。
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);
}