ASP.NET Core 防止跨站請求偽造(XSRF/CSRF)攻擊
什麼是反偽造攻擊?
跨站點請求偽造(也稱為XSRF或CSRF,發音為see-surf)是對Web託管應用程式的攻擊,因為惡意網站可能會影響客戶端瀏覽器和瀏覽器信任網站之間的互動。這種攻擊是完全有可能的,因為Web瀏覽器會自動在每一個請求中傳送某些身份驗證令牌到請求網站。這種攻擊形式也被稱為 一鍵式攻擊 或 會話控制 ,因為攻擊利用了使用者以前認證的會話。
CSRF攻擊的示例:
- 使用者登入 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>
請注意,表單的Action屬性將請求傳送到易受攻擊的網站,而不是惡意網站。這是CSRF的“跨站點”部分。
- 使用者點選提交按鈕,瀏覽器會自動包含請求站點(在這種情況下為易受攻擊的站點)的認證Cookie。
- 請求在擁有使用者身份驗證上下文的服務端執行,並且可以執行允許經過身份驗證使用者執行的任何操作。
此示例需要使用者單擊表單按鈕,惡意頁面也可以通過以下方式:
- 自動執行提交表單的指令碼。
- 通過AJAX請求傳送表單提交。
- 通過CSS隱藏的表單。
使用SSL不能阻止CSRF攻擊,惡意網站可以傳送https://
請求。
針對GET
請求站點的攻擊,可以使用Image
元素來執行(這種形式的攻擊在允許圖片的論壇網站上很常見)。使用GET
因為瀏覽器將所有相關的Cookie傳送到目標網站,所以可以針對使用Cookie進行身份驗證的網站進行CSRF攻擊。然而,CSRF攻擊並不僅限於利用Cookie,例如,Basic和Digest身份驗證也很脆弱。使用者使用Basic或Digest身份驗證登入後,瀏覽器將自動傳送憑據,直到會話(Session)結束。
注意:在這本文中,Session是指使用者進行身份驗證的客戶端會話。它與伺服器端會話或Session中介軟體無關。
使用者可以通過以下方式防範CSRF漏洞:
- 網站使用完畢後,登出會話。
- 定期清理瀏覽器的Cookie。
然而,CSRF漏洞根本上是Web應用程式的問題,而不是依靠使用者來解決。
ASP.NET Core MVC是如何處理CSRF的?
在ASP.NET Core MVC 2.0中,FormTagHelper為HTML表單元素注入防偽造令牌。例如,Razor檔案中的以下標記將自動生成防偽令牌:
<form method="post">
<!-- form markup -->
</form>
在以下情況為HTML格式元素自動生成防偽令牌:
- 該
form
標籤包含method="post"
屬性- action屬性為空(
action=""
) 或者 - 未提供action屬性(
<form method="post">
)。
- action屬性為空(
您可以通過以下方式禁用自動生成HTML表單元素的防偽令牌:
- 明確禁止
asp-antiforgery
,例如
<form method="post" asp-antiforgery="false">
</form>
- 通過使用標籤幫助器! 禁用語法,從標籤幫助器轉化為表單元素。
<!form method="post">
</!form>
- 在檢視中移除
FormTagHelper
,您可以在Razor檢視中新增以下指令移除FormTagHelper
:
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
提示:
Razor頁面會自動受到XSRF/CSRF的保護。您不必編寫任何其他程式碼,有關詳細資訊,請參閱XSRF/CSRF和Razor頁面。
防禦CSRF攻擊的最常見方法是令牌同步模式(STP)。STP是當用戶請求表單資料頁面時使用的技術。伺服器將與當前使用者的標識相關聯的令牌傳送給客戶端。客戶端將令牌發回伺服器進行驗證。如果伺服器接收到與驗證使用者身份不匹配的令牌,則該請求將被拒絕。令牌是唯一的,並且是不可預測的。令牌也可用於確保一系列請求的正確順序(確保頁面1在第2頁之前,頁面2在第3頁之前)。ASP.NET Core MVC模板中的所有表單都會生成防偽令牌,以下兩個示例演示在檢視邏輯中生成防偽令牌:
<form asp-controller="Manage" asp-action="ChangePassword" method="post">
</form>
@using (Html.BeginForm("ChangePassword", "Manage"))
{
}
您可以在不使用HTML標籤助手的情況下,向<form>
元素顯式新增防偽令牌@Html.AntiForgeryToken
:
<form action="/" method="post">
@Html.AntiForgeryToken()
</form>
在前面的例子中,ASP.NET Core將新增一個隱藏的表單欄位,類似於以下內容:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkSldwD9CpLRyOtm6FiJB1Jr_F3FQJQDvhlHoLNJJrLA6zaMUmhjMsisu2D2tFkAiYgyWQawJk9vNm36sYP1esHOtamBEPvSk1_x--Sg8Ey2a-d9CV2zHVWIN9MVhvKHOSyKqdZFlYDVd69XYx-rOWPw3ilHGLN6K0Km-1p83jZzF0E4WU5OGg5ns2-m9Yw" />
ASP.NET Core 包括三個過濾器用於防偽令牌的執行:ValidateAntiForgeryToken
、AutoValidateAntiforgeryToken
和 IgnoreAntiforgeryToken
。
ValidateAntiForgeryToken
ValidateAntiForgeryToken
是一個可應用於單個Action、控制器或全域性的操作過濾器。請求必須包含一個有效的令牌,否則對具有該過濾器Action的請求將被阻止。
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel account)
{
ManageMessageId? message = ManageMessageId.Error;
var user = await GetCurrentUserAsync();
if (user != null)
{
var result = await _userManager.RemoveLoginAsync(user, account.LoginProvider, account.ProviderKey);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
message = ManageMessageId.RemoveLoginSuccess;
}
}
return RedirectToAction(nameof(ManageLogins), new { Message = message });
}
ValidateAntiForgeryToken
特性標記的Action方法需要一個令牌,包括HTTP GET
請求。如果您全域性使用,您可以使用IgnoreAntiforgeryToken
特性來覆蓋它。
AutoValidateAntiforgeryToken
ASP.NET Core應用程式通常不會為HTTP安全方式(GET,HEAD,OPTIONS和TRACE)生成防偽令牌,而不是在全域性範圍內使用ValidateAntiForgeryToken
特性,然後用IgnoreAntiforgeryToken
特性覆蓋它,您可以使用AutoValidateAntiforgeryToken
特性。該特性與ValidateAntiForgeryToken
特性相似,但對以下HTTP請求方式不需要請求令牌:
- GET
- HEAD
- OPTIONS
- TRACE
我們建議您在非API場景中廣泛使用AutoValidateAntiforgeryToken
。這確保您的POST Action 預設受保護。另一種方式是在預設情況下忽略反偽造令牌,除非在個別Action方法標記了ValidateAntiForgeryToken
特性,不過在這種情況下,POST Action方法有可能不受保護,使您的應用程式容易受到CSRF攻擊。即使匿名的POST請求也應該傳送防偽令牌。
注意:API沒有自動機制來發送非Cookie的令牌;您的實現可能取決於您的客戶端程式碼的實現。
一些例子如下所示。
示例(控制器級別):
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
示例(全域性)
services.AddMvc(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
IgnoreAntiforgeryToken
IgnoreAntiforgeryToken
過濾器用於取消已經使用防偽標記的Action(或控制器)的需求。應用時,此過濾器將覆蓋在更高級別(全域性或控制器)上指定的過濾器ValidateAntiForgeryToken
和/或AutoValidateAntiforgeryToken
過濾器。
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
[HttpPost]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> DoSomethingSafe(SomeViewModel model)
{
// no antiforgery token required
}
}
JavaScript,AJAX和SPA(單頁應用程式)
在傳統基於HTML的應用程式中,使用隱藏的表單欄位將防偽令牌傳送到伺服器。在當前基於JavaScript的應用程式和單頁應用程式(SPA)中,許多請求以程式設計方式進行。這些AJAX請求可能會使用其它技術(如請求頭或Cookie)來發送令牌。如果使用Cookie來儲存身份驗證令牌,並在伺服器上驗證API請求,那麼CSRF將是一個潛在的問題,但是,如果使用本地儲存來儲存令牌,那麼CSRF漏洞可能會被減輕,因為本地儲存的值不會在每個請求時自動傳送到伺服器。因此,使用本地儲存將反偽造令牌儲存在客戶機上,並將令牌作為請求頭髮送,這是一種推薦的方式。
AngularJS
AngularJS通過約定來解決CSRF。如果伺服器傳送帶有名稱為XSRF-TOKEN
的Cookie ,則Angular的$http
服務將向該伺服器傳送的請求將該Cookie的值新增到請求頭。這個過程是自動的,您不需要明確設定請求頭。請求頭的名稱是X-XSRF-TOKEN
,伺服器會檢測該請求頭並驗證其內容。
對於ASP.NET Core API,使用此約定:
- 配置您的應用程式,在一個Cookie中提供一個稱為
XSRF-TOKEN
的令牌; - 配置防偽服務查詢名為
X-XSRF-TOKEN
的請求頭。
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
檢視示例。
JavaScript
在檢視中使用JavaScript,您可以在檢視中使用服務建立令牌,您將Microsoft.AspNetCore.Antiforgery.IAntiforgery
服務注入檢視並呼叫GetAndStoreTokens
,如下所示:
@{
ViewData["Title"] = "AJAX Demo";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
public string GetAntiXsrfRequestToken()
{
return Xsrf.GetAndStoreTokens(Context).RequestToken;
}
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<div class="row">
<input type="button" id="antiforgery" value="Antiforgery" />
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"></script>
<script>
$("#antiforgery").click(function () {
$.ajax({
type: "post",
dataType: "html",
headers:
{
"RequestVerificationToken": '@GetAntiXsrfRequestToken()'
},
url: '@Url.Action("Antiforgery", "Home")',
success: function (result) {
alert(result);
},
error: function (err, scnd) {
alert(err.statusText);
}
});
});
</script>
</div>
這種方法無需在伺服器設定Cookie或從客戶端讀取Cookie。
JavaScript還可以訪問Cookie中提供的令牌,然後使用Cookie的內容建立帶有令牌值的請求頭,如下所示。
context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken,
new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
然後,假設您構建的指令碼傳送的請求,將令牌傳送為一個名為X-CSRF-TOKEN
的請求頭中,請配置防偽服務以查詢X-CSRF-TOKEN
請求頭:
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
以下示例使用jQuery來建立一個包含相應請求頭AJAX請求:
var csrfToken = $.cookie("CSRF-TOKEN");
$.ajax({
url: "/api/password/changepassword",
contentType: "application/json",
data: JSON.stringify({ "newPassword": "ReallySecurePassword999$$$" }),
type: "POST",
headers: {
"X-CSRF-TOKEN": csrfToken
}
});
配置防偽
IAntiforgery
提供API來配置防偽系統。它可以在Startup
類的Configure
方法中使用。以下示例在應用程式的主頁生成防偽令牌,並將其作為Cookie傳送到響應中(使用上述預設命名約定):
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if ( string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) || string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// We can send the request token as a JavaScript-readable cookie,
// and Angular will use it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
}
return next(context);
});
}
選項
您可以在ConfigureServices
方法中定製防偽選項:
services.AddAntiforgery(options =>
{
options.CookieDomain = "mydomain.com";
options.CookieName = "X-CSRF-TOKEN-COOKIENAME";
options.CookiePath = "Path";
options.FormFieldName = "AntiforgeryFieldname";
options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
options.RequireSsl = false;
options.SuppressXFrameOptionsHeader = false;
});
選項 | 描述 |
---|---|
CookieDomain | Cookie的域名。預設為null 。 |
CookieName | Cookie的名稱。如果未設定,系統將生成一個以DefaultCookiePrefix (".AspNetCore.Antiforgery")開頭的唯一名稱。 |
CookiePath | Cookie設定的路徑。 |
FormFieldName | 在檢視中隱藏表單欄位的名稱。 |
HeaderName | 防偽系統使用的請求頭的名稱。如果null ,系統將僅使用表單資料。 |
RequireSsl | 指定防偽系統是否需要SSL。預設為false 。如果為true ,非SSL請求會失敗。 |
SuppressXFrameOptionsHeader | 指定是否禁止X-Frame-Options 響應頭的生成。預設情況下,響應頭生成的值為“SAMEORIGIN”。預設為false 。 |
擴充套件防偽
IAntiForgeryAdditionalDataProvider型別允許開發者擴充套件anti-XSRF系統的行為,在每個令牌中的增加額外資料。每次建立令牌時會呼叫GetAdditionalData方法,並且返回的值被嵌入生成的令牌內。實現者可以返回時間戳、隨機數或任何其它值,然後在驗證令牌時呼叫ValidateAdditionalData來驗證此資料。客戶的使用者名稱已經嵌入到生成的令牌中,因此不需要包含此資訊。如果令牌包含補充資料但沒有配置IAntiForgeryAdditionalDataProvider
,則補充資料不被驗證。
常用場景
CSRF攻擊依賴於瀏覽器預設行為,向站點發出請求同時,會發送與站點相關聯的Cookie。這些Cookies儲存在瀏覽器中,它們經常用於經過身份驗證的使用者提供會話Cookie。基於Cookie的身份驗證是一種非常流行的身份驗證模式。基於令牌的認證系統越來越受歡迎,特別是對於SPA和其它“智慧客戶端”場景。
基於Cookie的身份驗證
一旦使用者使用他們的使用者名稱和密碼進行身份驗證,就會發出一個令牌,用於標識它們並驗證它們是否經過身份驗證。令牌儲存為Cookie,客戶端所做的每個請求都會附帶令牌。生成和驗證此Cookie是由Cookie身份驗證中介軟體完成的。ASP.NET Core提供了將使用者主體序列化為加密Cookie的Cookie 中介軟體,然後在隨後的請求中驗證Cookie,重新建立主體並將其分配給HttpContext
的User
屬性。
當使用Cookie時,身份驗證Cookie只是表單身份驗證憑證的一個容器。在每個請求中,票據作為的表單認證Cookie的值傳遞並通過表單身份驗證,在服務端,以標識經過身份驗證的使用者。
當用戶登入到系統時,會在伺服器端建立使用者會話,並將其儲存在資料庫或其他持久儲存中,系統生成指向資料儲存中的會話金鑰,並將其作為客戶端Cookie傳送。每當使用者請求需要授權的資源時,Web伺服器將檢查此會話金鑰,系統檢查關聯的使用者會話是否具有訪問請求的資源的許可權。如果是,請求繼續;否則,請求返回為未授權。在這種方法下,Cookie的使用,使應用程式看起來是有狀態的,因為它能夠“記住”使用者以前已經在服務端完成了身份驗證。
使用者令牌
基於令牌的身份驗證不會在伺服器上儲存會話。相反,當用戶登入時,將頒發令牌(不是防偽令牌)。該令牌儲存驗證令牌所需的所有資料,它還包含使用者資訊,以claims的形式。當用戶想要訪問需要身份驗證的伺服器資源時,會使用 Bearer {token} 形式的附加授權頭髮送令牌給伺服器。這使得應用程式無狀態,因為在每個後續請求中,令牌在請求中傳遞給伺服器端驗證。該令牌未 加密 , 而是 編碼 。在伺服器端,令牌可以被解碼以訪問令牌內的原始資訊。要在隨後的請求中傳送令牌,您可以將其儲存在瀏覽器的本地儲存或Cookie中。如果您的令牌儲存在本地儲存中,則不必擔心XSRF漏洞,但如果令牌儲存在Cookie中,則會出現問題。
多個應用程式託管在一個域中
即使example1.cloudapp.net
和example2.cloudapp.net
是不同的主機,在.cloudapp.net
域內的所有主機之間存在一種隱式信任關係。這種隱式信任關係允許潛在的不受信任的主機影響彼此的Cookie(管理AJAX請求的同源策略不一定適用於HTTP Cookie)。ASP.NET Core執行時提供了一些緩解,使用者名稱被嵌入到欄位令牌中,因此即使惡意子域能夠覆蓋會話令牌,它將無法為使用者生成有效的欄位令牌。然而,當託管在這樣的環境中,內建的反XSRF例程仍然無法防止會話劫持或登入CSRF攻擊。共享主機環境對會話劫持、登入CSRF和其它攻擊都是不可控制的。