ASP.NET Core MVC 全面啟用 CSRF 防護的簡潔做法
| | | 6 | |
防止 CSRF 攻擊已是網頁的資安基本要求,這個議題之前有討論過:
- ASP.NET MVC 防止 AJAX POST CSRF 攻擊
- SPA + ASP.NET Core Minimal API 練習 - 實作 Windows 登入與 CSRF 防護
- 【茶包射手日記】網站在 localhost 測試正常,部署後出現 AntiForgeryToken 錯誤
簡單來說,ASP.NET 的 CSRF 防護機制是會產生一個唯一且無法預測的「防偽權杖 (Antiforgery Token)」,權杖會被分成兩個部分,一部分放在 Cookie 讓攻擊者無法使用 JavaScript 讀取,另一部分以 <input type="hidden"> 隱藏欄位嵌入表單內容,當使用者 POST 送出表單時,伺服器會驗證 Cookie 與隱藏欄位值,若缺少任何一個或對不上就直接拒絕請求,讓惡意人士難以透過偽造 POST 請求進行攻擊。
最近起了 ASP.NET Core 專案,重新研究整理,找到一次全站啟用 CSRF 防護,不用到處加 @Html.AntiForgeryToken() 及 [ValidateAntiForgeryToken] 的簡潔做法,寫成筆記備忘。
參考資料:Antiforgery in ASP.NET Core
我用最簡單的 Minimal API 專案(dotnet new web)當範例,在 Program.cs 加上 AddControllersWithViews() 啟用 MVC 並加上 AutoValidateAntiforgeryTokenAttribute,其他類型專案若使用 AddMvc、MapControllers... 則可省略[註1]:
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
var app = builder.Build();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
註1:.NET 8.0 之後,只要使用 MapRazorPages、MapControllers (或 MapControllerRoute) 或 MapRazorComponents 任何一種端點對應方法,Antiforgery 中介軟體就會被自動加入並啟用。參考 而 AddMvc() 內部會呼叫 AddControllersWithViews() 加 AddRazorPages(),底層會呼叫前面提到的端點對應方法,也會啟用防偽中介層。參考 (感謝 Jay Skyworker 補充)
註2:若要在 Minimal API 的 MapPost() 加上 CSRF 防護,可參考官方文件範例
如此,ASP.NET Core 會針對所有 TRACE、OPTIONS、HEAD、GET 以外的所有方法(POST、PUT、DELETE...)啟用防偽權杖檢查。
至於 View 端,可在 /Views/_ViewsImports.cshtml 引用 Microsoft.AspNetCore.Mvc.TagHelpers:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
如此,FormTagHelper 會自動為 .cshtml 中的 <form method="post"> 注入(Inject)防偽權杖隱藏欄位,不必手動寫 @Html.AntiForgeryToken()。自動注入條件為 form 包含 method="post" 且 action 為空值或未指定。若要避免加入防偽權杖欄位,可加上 asp-antiforgery="false" <form method="post" asp-antiforgery="false"> 或寫成 <!form ...> 針對 form 停用 TagHelper ( !符號的術語叫 Tag Helper Opt-Out Symbol):
<!form method="post">
<!-- ... -->
</!form>
如此,以下這段 .cshtml:
<form method="post">
<input type="text" name="data" placeholder="Text Value" />
<button type="submit" class="btn btn-primary">Submit</button>
</form>
實際產生的 HTML 在表單結尾會多一個名為 __RequestVerificationToken 的隱藏欄位:
<form method="post">
<input type="text" name="data" placeholder="Text Value" />
<button type="submit" class="btn btn-primary">Submit</button>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8....VbG0E" /></form>
使用 F12 開發者工具也可觀察到 ASP.NET Core 自動產生名為 .AspNetCore.Antiforgery.<編碼值> 的 Cookie:

Controller 端什麼都不用做,Middleware 中介層會在 [HttPost] 自動檢查,若防偽權杖無效則回傳 HTTP 400。
public class HomeController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index(string data)
{
return Content($"Received data: {data}");
}
}
要怎麼驗證 ASP.NET Core 真的有檢查防偽權杖?有兩種簡單做法,用瀏覽器 F12 工具就能操作。
方法一,刪掉 Cookie:

方法二,改掉權杖隱藏欄位:

以上任一種修改都會導致送出表單時得到 HTTP 400 錯誤,故得證:

至於 AJAX 呼叫,使用以下範例展示。由於 .cshtml 沒使用 <form method="post">,不會自動加上防偽權杖隱藏欄,故要自己加上 @Html.AntiForgeryToken(),使用 .fetch() 傳送 POST 請求時,用 document.querySelector('input[name="__RequestVerificationToken"]')?.value 取值,設成 RequestVerificationToken Header 就 OK 了。
<div>
<h1>AJAX Test</h1>
<table>
<tr>
<td><label for="name">Name:</label></td>
<td><input type="text" id="name" placeholder="Enter name" /></td>
</tr>
<tr>
<td><label for="count">Count:</label></td>
<td><input type="number" id="count" placeholder="Enter count" /></td>
</tr>
<tr>
<td colspan="2" style="text-align: right;">
<button type="button" onclick="submitData()">Submit via AJAX</button>
</td>
</tr>
</table>
<p id="response"></p>
</div>
@Html.AntiForgeryToken()
<script>
function submitData() {
const name = document.getElementById('name').value || 'N/A';
const count = document.getElementById('count').value || '-1';
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
fetch('/Home/Ajax', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify({ Name: name, Count: parseInt(count) })
})
.then(response => {
debugger;
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return response.text();
})
.then(result => {
showResponse(result);
})
.catch(error => {
showResponse('Error: ' + error.message);
});
}
function showResponse(message) {
document.getElementById('response').innerHTML = message;
}
</script>
伺服器端依一般寫法,不需額外設定也會檢查防偽權杖:
[HttpGet]
public IActionResult Ajax()
{
return View();
}
public record Entity(string Name, int Count);
[HttpPost]
public IActionResult Ajax([FromBody]Entity data)
{
return Json(data);
}
正常情況如下:

故意開 F12 清掉 Cookie,請求會被擋下來,測試成功!

以上就是在 ASP.NET Core 網站全面啟用 CSRF 防護的簡潔做法展示,範例專案已上傳到 Github,有需要的同學可自取參考。
Demonstrates enabling global CSRF protection in ASP.NET Core using AutoValidateAntiforgeryToken, with automatic tokens for forms and AJAX.
Comments
# by Tim
感謝分享
# by Miltonchu
謝謝大神😀,我也在回頭看看一些以前的專案,有遺漏的地方用這個補強 (這個防機器人的校驗我差點過不了😂😂😂)
# by m
話說像是 Checkmarx 與 fortify 若沒有看到 @Html.AntiForgeryToken() , RequestVerificationToken 之類的會不會該該叫?
# by Jeffrey
to m,感覺非常有可能... (補聲暗)
# by 小黑
讚啦
# by ALEX
我的用法是 builder.Service.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); 在 View @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf @{ var XSRFToken = Xsrf.GetAndStoreTokens(Context).RequestToken; } 在 axios / ajax /fetch 加入 HEADER 'X-XSRF-TOKEN': '@XSRFToken' 例如: var config = { headers: { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': '@XSRFToken' } }; axios.post(url, postData, config) .then((response) => { //略 }) .catch((error) => { //略 });