目录
介绍
跨站点请求伪造,也称为CSRF(发音为“See-Surf”)、XSRF、一键攻击和会话骑乘,是一种攻击类型,攻击者强制用户在应用程序中执行不需要的操作,而用户已登录。攻击者诱骗用户代表他们执行操作。此攻击的影响取决于用户拥有的权限级别。例如,在易受攻击的银行网站中,攻击者可以从受害者的账户中转移一定数量的资金或取得整个账户的所有权。
关于源代码
本文示例的源代码可在GitHub上通过以下链接获得:
您在存储库中有五个项目:
- 攻击样本项目,AttackSample.AttackerApp和AttackSample.VulnerableApp其出示有效的CSRF攻击存在漏洞的应用程序。
- 安全样本项目,SecureSample.SecureApp和SecureSample.AttackerApp其显示出失败的CSRF攻击受保护的应用程序。
- Angular项目,独立存在的SecureSample.AngularApp。
CSRF示例
如果我们以银行网站为例,攻击者可能会诱使用户加载某些网站或链接,其中包含向银行网站发出伪造请求以转移一些资金的脚本。如果用户当前登录了银行网站,并且该网站容易受到此类攻击,则攻击者可能会成功。
例如,如果银行网站允许以下转账请求:
GET http://vulnerable-bank.com/transfer?amount=1000&to=12345
攻击者可以将他的银行帐号插入“to”字段,并将用户发送到一个页面,该页面包含一个脚本,可以发出上述请求或鼓励他/她点击某个链接,例如:
<a href="http://vulnerable-bank.com/transfer?amount=1000&to=98765">Read More!</a>
或者将请求嵌入到一个假的0x0图像中:
<img src="http://vulnerable-bank.com/transfer?amount=1000&to=98765" width="0" height="0" />
攻击者可以将他的代码包含在用户经常访问的站点中(例如,将链接放入某些下载论坛),或者在一些社会工程的帮助下分发他的页面,例如通过电子邮件或聊天发送链接。
如果用户已经登录了银行账户,当他/她打开页面或点击虚假链接时,浏览器会自动包含目标网站cookies等数据并执行伪造请求,导致转账请求成功。
攻击剖析
总之,成功的CSRF攻击包括:
- 易受攻击的网站
- 当前登录到该网站的用户
- 浏览器可能包含在请求中的会话和其他用户cookie
- 易于预测的请求参数
- 用户访问有害页面或点击虚假链接,对易受攻击的网站执行伪造请求
保护您的Web应用程序
为了保护您的Web应用程序免受此类攻击,请始终:
- 使用不可预测的参数。使攻击者难以模拟或构建对您的应用程序的请求。使用令牌的不可预测参数的示例,将很快解释。
- 在每种情况下和每一步都严格验证。
CSRF攻击实战
在我们的代码示例中,我们有两个AttackSample项目,一个用于易受攻击的应用程序,另一个用于攻击者应用程序。在易受攻击的应用程序中,对用户进行身份验证,并创建一个cookie来保存用户数据:
[HttpGet]
public bool IsAuthenticated()
{
return Context.Request.Cookies["IsAuthenticated"] == "1";
}
[HttpPost]
public IActionResult Login()
{
Context.Response.Cookies.Append("IsAuthenticated", "1");
return RedirectToAction(nameof(Index), "Home");
}
[HttpPost]
public IActionResult Logout()
{
Context.Response.Cookies.Delete("IsAuthenticated");
return RedirectToAction(nameof(Index), "Home");
}
该应用程序维护一个balance对象,并允许对借方和贷方操作进行易于预测的请求:
[HttpGet]
public int Balance()
{
return CurrentBalance;
}
[HttpPost]
public int Debit(int amount)
{
CurrentBalance -= amount;
return Balance();
}
[HttpPost]
public int Credit(int amount)
{
CurrentBalance += amount;
return Balance();
}
另一方面,攻击者通过在用户单击链接时向他承诺礼物来诱使用户执行虚假请求:
<form method="post" action="http://localhost:62833/api/Debit">
<input type="hidden" name="amount" value="50" />
<button type="submit">Click here to win a free iPhone!</button>
</form>
用户被诱骗执行虚假请求并导致成功转移金额。
防伪令牌
保护您的网站免受此类攻击的关键是使用不可预测的请求参数。这些不可预测的参数之一是防伪令牌。
防伪令牌,也称为CSRF令牌,是由服务器端应用程序为客户端发出的后续HTTP请求生成的唯一的、秘密的、不可预测的参数。发出该请求时,服务器会根据预期值验证此参数,如果令牌丢失或无效,则拒绝该请求。
所以,基本上,以下请求:
GET http://bank.com/transfer?amount=1000&to=12345
将扩展为第三个参数:
GET http://bank.com/transfer?amount=1000&to=12345&token=32465468465468465165484654768732467655465
这个令牌是巨大的,无法猜测。服务器将仅为后续请求包含该令牌,并且每次提供页面/表单时都会生成一个新令牌。
从技术上讲,防伪令牌不是在查询字符串中发送的参数。事实上,它是一个cookie,表示为您在表单中生成的隐藏字段。提交表单时,此值将作为标头与请求一起包含在内。服务器代码将检查请求并验证从客户端发送的值。
ASP.NET Core中的防伪
默认情况下,新的ASP.NET Core Razor引擎将包含页面表单的防伪令牌,您只需添加相应的验证即可。尽管如此,接下来的几节将让您了解如何在您的应用程序中生成防伪令牌以及如何验证它们。
令牌生成:手动方式
有两种方法可以生成和验证防伪令牌,我们将从不方便的手动方式开始。这可以通过使用IAntiForgery服务来完成。
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Csrf
@functions {
public string GenerateCsrfToken()
{
return Csrf.GetAndStoreTokens(Context).RequestToken;
}
}
<form method="post">
<input type="hidden" id="RequestVerificationToken"
name="RequestVerificationToken" value="@GenerateCsrfToken()" />
</form>
该GetAndStoreTokens方法生成请求令牌并将令牌存储在响应cookie中。您可以使用该RequestToken属性访问生成的令牌。
生成的隐藏字段将如下所示(为清楚起见,已将标记缩写):
<input type="hidden" id="RequestVerificationToken"
name="RequestVerificationToken" value="CfDJ8El14QZHDe5Dtl0m3qOu6_PbEHcKAJ5ZjSRj6iF...">
手动生成CSRF令牌的另一种更清晰的方法是使用MVC HTML帮助程序:
<form method="post">
@Html.AntiForgeryToken()
</form>
您可以使用Chrome DevTools检查生成的cookie:
令牌生成:自动方式
正如我们之前所说,新的ASP.NET Core Razor引擎将始终为您生成CSRF令牌,但是,您仍然可以控制令牌生成过程。
您可以使用AddAntiforgery方法自定义 Razor 页面的令牌生成过程,该方法可以在您的Startup.ConfigureServices方法中调用的。
services.AddAntiforgery(options =>
{
options.FormFieldName = "AntiForgeryFieldName";
options.HeaderName = "AntiForgeryHeaderName";
options.Cookie.Name = "AntiForgeryCookieName";
});
前面的代码将为具有指定名称的令牌生成一个隐藏字段,该令牌将与具有指定标头名称的请求一起发送。
<input name="AntiForgeryFieldName" type="hidden" value="CfDJ8N1DZWaKEuhDio...">
默认情况下,ASP.NET core中生成的cookie名称为“.AspNetCore.Antiforgery.<hash>”,字段名称为“__RequestVerificationToken”,头部名称为“RequestVerificationToken”。
令牌验证
现在是下一步,令牌验证。让我们以正常的、不舒服的方式开始。在您的目标操作中,您可以使用以下代码进行令牌验证:
private Microsoft.AspNetCore.Antiforgery.IAntiforgery Csrf { get; set; }
public ApiController(Microsoft.AspNetCore.Antiforgery.IAntiforgery csrf)
{
this.Csrf = csrf;
}
private async Task<bool> ValidateAntiForgeryToken()
{
try
{
await Csrf.ValidateRequestAsync(this.HttpContext);
return true;
}
catch (Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException)
{
return false;
}
}
[HttpPost]
public async Task<ActionResult<int>> Debit(int amount)
{
if (false == await ValidateAntiForgeryToken())
return BadRequest();
// action logic here
}
在前面的代码中,我们首先定义了我们的IAntiforgery服务。此服务允许您使用在令牌无效时引发AntiforgeryValidationException异常的ValidateRequestAsync方法来验证给定的请求。我们在我们的Debit方法中首先调用了这个函数,并为无效令牌返回了400 Bad Request响应。
验证CSRF令牌的另一种更清晰的方法是使用以下ValidateAntiForgeryToken属性:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult<int>> Debit(int amount)
上述代码将执行与手动IAntiforgery服务代码相同的功能。
现在,当我们运行我们的应用程序时,我们可以看到不同之处。该应用程序已针对CSRF攻击进行保护,攻击者无法向您的应用程序执行请求。
值得一提的是,ValidateAntiForgeryToken可以应用于控制器类,并将导致所有端点上的CSRF验证。我们还有一些使用ValidateAntiForgeryToken属性的替代方法:
- AutoValidateAntiForgeryToken属性:将自动验证端点除了所有HTTP方法GET,HEAD,OPTIONS和TRACE。
- IgnoreAntiforgeryToken属性:如果父类用ValidateantiForgeryToken或修饰,则将忽略验证中的方法AutoValidateAntiForgeryToken。
JavaScript 中的防伪
让我们再举一个例子。假设您使用JavaScript访问API,并使用以下代码调用该credit方法:
function request(url) {
let url = location.origin + "/api/credit?amount=10";
var request = {};
request.url = url;
request.type = 'POST';
request.success = function (balance) {
$('#balance')[0].innerText = balance;
};
request.error = function (xhr) {
alert(`${xhr.status} ${xhr.statusText}`);
};
$.ajax(request);
}
当防伪验证运行时,您将收到400 bad request错误,这是意料之中的,因为ASP.NET Core引擎找不到CSRF令牌标头。
为此,我们必须手动将我们的CSRF令牌添加到我们的请求标头列表中。对我们的代码稍作改动即可解决问题:
function request(url) {
let url = location.origin + "/api/debit?amount=10";
var request = {};
request.url = url;
request.type = 'POST';
request.headers = {
'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
};
request.success = function (balance) {
$('#balance')[0].innerText = balance;
};
request.error = function (xhr) {
alert(`${xhr.status} ${xhr.statusText}`);
};
$.ajax(request);
}
正如我们之前所说,CSRF令牌的默认标头名称是“RequestVerificationToken”。如果您出于任何原因更改了默认标头名称:
services.AddAntiforgery(options =>
{
options.HeaderName = "AntiForgeryHeaderName";
});
然后,您必须更改JavaScript代码中的值:
request.headers = {
'AntiForgeryHeaderName': $('input[name="__RequestVerificationToken"]').val()
};
Angular中的防伪
通常,当从Angular应用程序访问受CSRF保护的端点时,如果您没有指定CSRF标头,您将收到400个错误的请求。
要处理此问题,您必须了解以下内容:
- 只有当它作为cookie存储在Angular的专用名称下时,Angular才会识别CSRF令牌,即“XSRF-TOKEN”。
- Angular将始终将cookie令牌作为专用名称“X-XSRF-TOKEN”下的标头发送。
- 您的应用程序必须能够在Angular的专用名称下生成CSRF cookie,并在Angular的专用名称下验证CSRF标头。
让我们看看我们如何做到这一点:
public void Configure
(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
app.Use((context, next) =>
{
// return current accessed path
string path = context.Request.Path.Value;
if (path.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1)
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next();
});
}
代码相当简单,它执行以下操作:
- 它首先定义一个将为每个请求执行的中间件。该中间件将负责生成具有专用名称的CSRF cookie。
- 在这个中间件中,它检查请求的路径。您不需要为所有请求生成cookie,您只需要为API目标请求生成cookie。从技术上讲,您需要为针对我们的API控制器“/api/ ”的请求生成此 cookie
- 接下来,代码使用该IAntiforgery服务生成CSRF令牌并将其存储在使用该GetAndStoreTokens方法的cookie中。我们之前在手动令牌生成部分中使用过这种机制。除非您使用AddForgery方法(前面提到)更改默认cookie名称,否则生成的令牌的默认名称将以“.AspNetCore.Antiforgery”开头,正如我们之前所说。
- 最后,该代码读取生成的令牌并将其存储在cookie中以用于发送请求。它使用专用的Angular cookie名称“XSRF-TOKEN”。
下一步是配置生成的应用程序以读取正确的标头。正如我们之前所说,我们必须保留默认的Angular CSRF标头名称“X-XSRF-TOKEN”。
services.AddAntiforgery(opts =>
{
opts.HeaderName = "X-XSRF-TOKEN";
});
现在,运行应用程序并查看charm。我们不再收到400 bad request错误。并且在使用 Chrome DevTools或Fiddler调查请求时,我们可以清楚地看到生成的cookie和标头名称。
Angular绝对路径问题
如果请求的路径是相对的,上面的代码将像charm一样工作:
debit(amount: number): Observable<number> {
// relative path
let url = `/api/debit?amount=${amount}`;
return this.request(url);
}
request(url: string): Observable<number> {
let result: number;
return this.http.post<number>(url, { });
}
但是,当请求的路径是绝对路径时,即使是同一主机,Angular也不够聪明,无法在请求中包含CSRF令牌。您必须手动将其包含在您的请求中。例如,以下代码将不起作用:
credit(amount: number): Observable<number> {
// absolute path
let url = this.baseUrl + `api/credit?amount=${amount}`;
return this.request(url);
}
这种情况的解决方法是手动将header添加到请求中,或者使用HTTP拦截器自动将其添加到所有请求中。让我们看看如何定义我们的拦截器。我们将从拦截器类本身开始。
import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpXsrfTokenExtractor } from "@angular/common/http";
import { HttpEvent, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable()
export class XsrfInterceptor implements HttpInterceptor {
constructor(private xsrfTokenExtractor: HttpXsrfTokenExtractor) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// load token
let xsrfToken = this.xsrfTokenExtractor.getToken();
if (xsrfToken != null) {
// create a copy of the request and
// append the XSRF token to the headers list
const authorizedRequest = req.clone({
withCredentials: true,
headers: req.headers.set('X-XSRF-TOKEN', xsrfToken)
});
return next.handle(authorizedRequest);
} else {
Return next.handle(req);
}
}
}
上面的代码只是使用HttpXsrfTokenExtractor处理从cookie中提取令牌的服务来提取令牌。然后它复制当前请求并使用专用名称“X-XSRF-TOKEN”附加CSRF标头。最后,通过管道将请求传递给下一个处理程序。
要使用该HttpXsrfTokenExtractor服务,您需要在应用程序导入中包含其模块。
imports: [
HttpClientModule,
],
最后,您必须将拦截器包含在应用程序的可注入对象中。
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: XsrfInterceptor, multi: true }
],
现在您可以运行该应用程序并查看它如何针对所有请求顺利运行。
值得一提的是,如果您的Angular应用程序包含在Razor CSHTML文件中,您可以使用常规方式简单地生成CSRF令牌:
<div>
<app-root></app-root>
@Html.AntiForgeryToken()
</div>
在Angular代码中,您可以使用jQuery样式来获取令牌的值,并使用如下代码将其包含在您的请求中:
declare var $: any;
const httpOptions = {
headers: new HttpHeaders({
'X-XSRF-Token': $('input[name=__RequestVerificationToken]').val()
})
};
this.http.post(url, httpOptions);
概括
到本文结束时,我认为您已经全面了解CSRF攻击的工作原理以及如何保护您的应用免受这些攻击。
同样,示例的源代码可在GitHub上通过以下链接获得:
您在存储库中有五个项目:
- 攻击样本项目,AttackSample.AttackerApp和AttackSample.VulnerableApp其出示有效的CSRF攻击存在漏洞的应用程序。
- 安全样本项目,SecureSample.SecureApp和SecureSample.AttackerApp其显示出失败的CSRF攻击受保护的应用程序。
- Angular 项目,独立存在的SecureSample.AngularApp。
https://www.codeproject.com/Articles/5297793/Preventing-CSRF-Attacks-using-ASP-NET-Core-JavaScr