防止 CSRF 攻擊已是網頁的資安基本要求,這個議題之前有討論過:

簡單來說,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) => { //略 });

Post a comment