تحول در امنیت ASP.NET Core 11: بررسی عمیق مکانیسم خودکار و بدون پیکربندی مقابله با حملات CSRF
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۴/۳۱ ۰۹:۴۵
آدرس: www.dntips.ir
WebApplication.CreateBuilder() فعال بوده و بدون نیاز به مدیریت توکن، درخواستهای ناامن و مشکوک Cross-Origin را مسدود میسازد. در این مقاله به بررسی این معماری، الگوریتم تصمیمگیری، ارزیابی تنبل (Lazy Evaluation)، نحوه یکپارچهسازی با سیستمهای موجود و نکات زیرساختی این قابلیت میپردازیم.app.UseAntiforgery() فراخوانی شده و تمامی فرمها شامل توکن مربوطه هستند.Sec-Fetch-Site و Origin معرفی شدند. فریمورک ASP.NET Core 11 از این هدرها بهره میجوید تا نوع منبع درخواست را احراز کند؛ بدون اینکه نیازی به بررسی توکن در تمام لایهها باشد.[ ورود درخواست HTTP ]
│
▼
آیا متد امن است؟ (GET, HEAD, OPTIONS, TRACE) ─────────► [ مجاز / Allowed ]
│ خیر
▼
آیا مبدأ در TrustedOrigins (خطمشی CORS) است؟ ─────────► [ مجاز / Allowed ]
│ خیر
▼
آیا هدر Sec-Fetch-Site موجود است؟
├─► مقادیر "same-origin" یا "none" ───────────► [ مجاز / Allowed ]
└─► مقادیر دیگر ("cross-site", "same-site") ──► [ غیرمجاز / Denied ]
│ (موجود نیست)
▼
آیا هدر Origin موجود است؟
├─► مطابقت با Host درخواست ──────────────────► [ مجاز / Allowed ]
└─► عدم مطابقت با Host درخواست ──────────────► [ غیرمجاز / Denied ]
│ (موجود نیست)
▼
درخواست بدون هدر مرورگر (مانند cURL, Postman) ───────► [ مجاز / Allowed ]GET، HEAD، OPTIONS و TRACE وضعیت سیستم را تغییر نمیدهند؛ بنابراین همواره مجاز تلقی میشوند.AddCors) ثبت شدهاند (مثلاً policy.WithOrigins("[https://sso.example.com](https://sso.example.com)")) بهصورت خودکار مجاز شناخته میشوند.نکته امنیتی: سیاستهایی که از AllowAnyOrigin() استفاده میکنند نادیده گرفته میشوند، زیرا اعتماد به تمام دامنهها هدف اصلی مقابله با CSRF را نقض میکند.Sec-Fetch-Site یا Origin را ارسال نمیکنند، متوقف نشده و مجاز دانسته میشوند.CsrfProtectionMiddleware است. این میانافزار درخواستهای Cross-Origin نامعتبر را بلافاصله قطع (Short-Circuit) نمیکند.IAntiforgeryValidationFeatureIsValid = true یا IsValid = false) را روی رابط IAntiforgeryValidationFeature در HttpContext ثبت کرده و اجازه میدهد درخواست به سمت لایههای پایینتر (Downstream) حرکت کند.// نمایی مفهومی از ثبت حکم در Context
httpContext.Features.Set<IAntiforgeryValidationFeature>(new AntiforgeryValidationFeature
{
IsValid = isAllowed,
// ...
});IsValid == false دریافت کند، هیچ اتفاق بالادستی رخ نمیدهد؛ مگر زمانی که یک کامپوننت یا Endpoint تصمیم به خوانش بدنه فرم (Form Consumption) بگیرد.[FromForm]، یا ارسال فرم در Blazor SSR). در این حالت، مصرفکننده ویژگی IAntiforgeryValidationFeature را بررسی کرده، متوجه حکم IsValid == false شده و درخواست را با خطای HTTP 400 Bad Request رد میکند.IAntiforgeryValidationFeature را بازخوانی کرده و در صورت نادرست بودن حکم، برگرداندن پاسخ ۴۰۰ را به عهده میگیرند:AntiforgeryMiddlewareAuthorizationFilter (معماری MVC / Razor Pages)RequestDelegateFactory (در Minimal APIs هنگام بایندینگ متغیرهای [FromForm])FormFeature (به عنوان لایه پشتیبان هنگام بازخوانی مستقیم HttpContext.Request.Form)RazorComponentEndpointInvoker (پردازشگر فرمها در Blazor SSR)app.UseAntiforgery() را داشته باشید، میدلور توکن پس از میدلور CSRF اجرا میشود. میدلور توکن به عنوان مرجع نهایی (Authoritative) عمل کرده، حکم قبلی CSRF را پاک میکند و نتیجه اعتبارسنجی توکن را جایگزین مینماید.IsValid = true خواهد بود.IsValid = false خواهد شد.RazorComponentEndpointInvoker مستقیماً متد IAntiforgery.ValidateRequestAsync را فراخوانی میکرد. در .NET 11:IAntiforgeryValidationFeature اعتماد میکند.app.UseAntiforgery() وجود ندارد، زیرا میدلور CSRF بهصورت خودکار تزریق شده و محافظت لایه اول را فراهم میسازد.Microsoft.AspNetCore.Antiforgery معرفی میکند:namespace Microsoft.AspNetCore.Antiforgery;
public interface ICsrfProtection
{
CsrfProtectionResult Validate(HttpContext httpContext);
}
public enum CsrfProtectionResult
{
Allowed,
Denied
}ICsrfProtectionو مسئله JIT CompilerICsrfProtection درون اسمبلی Microsoft.AspNetCore.Antiforgery.dll باقی بماند. اما در تستهای یکپارچهسازی مشکلی کشف شد:ConfigureWebDefaultsWorker در فرایند ساخت برنامه، متد زیر را اجرا میکرد:services.TryAddSingleton<ICsrfProtection>(...);
ConfigureWebDefaultsWorker (که در تمامی پروژههای مبتنی بر WebApplication.CreateBuilder() اجرا میشود)، مجبور به بارگذاری اسمبلی حاوی پارامتر ژنریک ICsrfProtection بود.Microsoft.AspNetCore.Antiforgery.dll در خروجی bin خود با خطا مواجه میشدند.ICsrfProtection را به اسمبلی پایه Microsoft.AspNetCore.Http.Abstractions منتقل کرد تا بدون ایجاد وابستگیهای اضافی به سایر اسمبلیها، در تمامی پروژهها قابل استفاده باشد.ICsrfProtectionusing Microsoft.AspNetCore.Antiforgery;
var builder = WebApplication.CreateBuilder(args);
// ثبت پیادهسازی سفارشی (توجه: TryAddSingleton در اینجا پاسخگو نیست)
builder.Services.AddSingleton<ICsrfProtection, CustomCsrfProtection>();
var app = builder.Build();
app.MapPost("/api/data", ([FromForm] MyModel data) => Results.Ok(data));
app.Run();
public class CustomCsrfProtection : ICsrfProtection
{
public CsrfProtectionResult Validate(HttpContext httpContext)
{
// منطق سفارشی بررسی هدرها یا آدرسها
var customHeader = httpContext.Request.Headers["X-Custom-Source"];
if (customHeader == "TrustedApp")
{
return CsrfProtectionResult.Allowed;
}
return CsrfProtectionResult.Denied;
}
}.DisableAntiforgery() یا ویژگی [IgnoreAntiforgeryToken] استفاده کنید:// در Minimal APIs
app.MapPost("/api/v1/webhook", () => Results.Ok())
.DisableAntiforgery();
// در MVC Controllers
[HttpPost]
[IgnoreAntiforgeryToken]
public IActionResult HandleExternalWebhook()
{
return Ok();
}appsettings.json:{
"CrossOriginProtection": "disable"
}WebHostBuilder:var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseSetting("CrossOriginProtection", "disable");builder.Services.RemoveAll<ICsrfProtection>();
| معیار مقایسه | سیستم جدید Cross-Origin Protection (.NET 11) | سیستم سنتی Token-Based Antiforgery |
| مبنای امنیت | هدرهای استاندارد مرورگر (Sec-Fetch-Site, Origin) | توکنهای رمزنگاریشده (Hidden Input / Cookie) |
| نیازمندی به پیکربندی | صفر (Zero Config بهصورت پیشفرض) | نیازمند تزریق میدلور و افزودن TagHelper در فرمها |
| تأثیر بر کارایی (Performance) | فوقالعاده سبک، بدون حالت (Stateless) | نیازمند پردازش رمزنگاری و تولید توکن |
| پشتیبانی از کلاینتهای غیر مرورگری | بهصورت خودکار مجاز دانسته میشوند | نیازمند مدیریت هدرهای سفارشی یا استثنا کردن مسیر |
| مکانیسم رد درخواست | ارزیابی تنبل (Lazy Evaluation) هنگام خواندن فرم | قطع زودهنگام در صورت عدم وجود توکن معتبر |
Form POST از سرویسهای ثالث (مانند درگاههای پرداخت، سرویسهای SMS، یا Webhook سامانههای دیگر) دریافت میکنند.Origin یا Sec-Fetch-Site: cross-site را ست میکند، سیستم جدید CSRF این درخواست را غیرمجاز تشخیص میدهد. به محض اینکه برنامه سعی کند فرم را بخواند ([FromForm] یا Request.Form)، درخواست با خطای HTTP 400 Bad Request رد میشود.راهکار: باید روی این مسیرهای خاص، محافظت ضد جعل را غیرفعال کنید:
app.MapPost("/api/payment-callback", ...).DisableAntiforgery();
// یا در کنترلرها
[IgnoreAntiforgeryToken]app.UseAntiforgery()را حذف کرده بودندMapRazorComponents()، طبق برخی ترفندها (Workarounds)، فراخوانی app.UseAntiforgery() را از فایل Program.cs حذف کرده بودند یا DisableCsrfProtection را تنظیم میکردند اما همچنان فرمهای Blazor SSR داشتند.RazorComponentEndpointInvokerدیگر خودش اعتبارسنجی توکن را انجام نمیدهد و کاملاً به حکم میانافزار بالادستی اعتماد میکند. اگر شخصی به صورت دستی این ویژگی جدید را با DisableCsrfProtection=true غیرفعال کرده باشد و فراخوانی app.UseAntiforgery() را هم در pipeline نداشته باشد، هنگام اجرای Blazor SSR با استثنا (Exception) مواجه میشود؛ زیرا سیستم متوجه میشود که هیچ میدلوری برای بررسی فرمها اجرا نشده است!app.example.com و api.example.com) استفاده میکرده.builder.Services.AddCors(options => options.AddPolicy("MyPolicy", ...))).AllowAnyOrigin() استفاده میکرده.CorsOptions قابلیت شمارش عمومی ندارند) و AllowAnyOrigin() را هم به دلایل امنیتی نادیده میگیرد. در نتیجه، درخواستهای POST بیندامنهای مشروع شما درون سازمان/شبکه خودتان، توسط سیستم جدید CSRF Denied تلقی شده و هنگام خواندن فرم با خطای 400 مواجه میشوند!راهکار: باید دامنههای موثق را حتماً در Default Policy مربوط به CORS ثبت کنید:
builder.Services.AddCors(options => {
options.AddDefaultPolicy(policy => {
policy.WithOrigins("https://app.example.com");
});
});HttpClient درون WebApplicationFactory در تستهای #C، یا ابزارهایی مثل Postman و Insomnia معمولاً هدرهای مرورگر را ارسال نمیکنند.Origin یا Referer غیرواقعی/ساختگی ست کرده باشید که با Host درخواست مطابقت نداشته باشد، تستهای قدیمی شما که قبلاً پاس میشدند، اکنون با خطای 400 شکست خواهند خورد.POST ناسالم به سرور میرسد، وارد میدلورها میشود، حتی از لایه Authentication و Authorization عبور میکند و وارد کد کنترلر/اکشن میشود! توسعهدهنده فکر میکند همه چیز درست است، اما به محض اینکه در اولین سطر اکشن، کد await Request.ReadFormAsync() یا Model Binding فرم اجرا میشود، ناگهان درخواست قطعی شده و خطای ۴۰۰ برمیگرداند. این تغییر رفتار نسبت به سیستمهای قدیمی که در همان ابتدای میدلور درخواست را قطع (Short-circuit) میکردند، ممکن است باعث سردرگمی در دیباگ لاگها شود..DisableAntiforgery() بگذارید.WebApplicationBuilder کار خود را به صورت پیشفرض انجام دهد.Data Protection متکی است. در معماریهای کلود (مثل Kubernetes) یا Web Farmها، همگامسازی و ذخیرهسازی امن این کلیدها میان نسخههای متعدد (Replicas) نیاز به تنظیمات پیچیده (مانند Redis یا Azure Key Vault) دارد.DefaultCsrfProtection)ICsrfProtection و پیادهسازی DefaultCsrfProtection معرفی شده که توسط CsrfProtectionMiddleware فراخوانی میشود. این الگوریتم که الگوی خود را از زبان Go 1.25 الهام گرفته است، درخواست را طی ۷ مرحله ارزیابی میکند:[ ورود درخواست به CsrfProtectionMiddleware ]
│
├── ۱. آیا متد HTTP امن است؟ (GET, HEAD, OPTIONS, TRACE, QUERY) ──> [ مجاز ]
├── ۲. آیا Sec-Fetch-Site برابر same-origin یا none است؟ ───────> [ مجاز ]
├── ۳. آیا Origin در لیست مجاز CORS endpoint هست؟ ───────────────> [ مجاز ]
├── ۴. آیا Sec-Fetch-Site وجود دارد؟ (مقادیر cross-site / same-site) ──> [ مسدود ]
├── ۵. آیا Origin موجود است؟
│ ├── تطابق Origin با هدر Host ─────────────────────────────> [ مجاز ]
│ └── عدم تطابق Origin با Host ─────────────────────────────> [ مسدود ]
└── ۶. هدرهای Sec-Fetch-Site و Origin وجود ندارند؟
└── درخواست غیرمرورگری است (مثل cURL / Postman) ───────────> [ مجاز ][FromForm] یا Request.Form). اگر درخواست شامل بدنه JSON باشد یا توسط یک ابزار CLI ارسال شده باشد، خطای CSRF صادر نمیشود.400 Bad Request بدهند.ITagHelperInitializer سفارشی، تولید توکن برای تمام تگهای غیرفعال میشود:public class DisableAntiForgeryTagHelperInitializer : ITagHelperInitializer<FormTagHelper>
{
public void Initialize(FormTagHelper helper, ViewContext context)
{
// غیرفعال کردن ساخت input مخفی توکن
helper.Antiforgery = false;
}
}
// ثبت در DI Container:
builder.Services.AddSingleton<ITagHelperInitializer<FormTagHelper>>(
new DisableAntiForgeryTagHelperInitializer());AutoValidateAntiforgeryTokenAttribute از الگوی MVC و Razor Pages از طریق یک Convention:public class RemoveAntiforgeryConvention : IPageApplicationModelConvention, IControllerModelConvention
{
public void Apply(PageApplicationModel model) => RemoveFilter(model.Filters);
public void Apply(ControllerModel controller) => RemoveFilter(controller.Filters);
private static void RemoveFilter(IList<IFilterMetadata> filters)
{
var antiforgeryFilter = filters.FirstOrDefault(f => f is AutoValidateAntiforgeryTokenAttribute);
if (antiforgeryFilter != null)
{
filters.Remove(antiforgeryFilter);
}
}
}
// اضافه کردن Convention در تنظیمات:
builder.Services.AddRazorPages(options => options.Conventions.Add(new RemoveAntiforgeryConvention()));
builder.Services.AddControllersWithViews(options => options.Conventions.Add(new RemoveAntiforgeryConvention()));CsrfProtectionMiddleware را ارزیابی کند:public class CsrfProtectionAuthorizationFilter : IAuthorizationFilter, IAntiforgeryPolicy
{
public void OnAuthorization(AuthorizationFilterContext context)
{
// بررسی وضعیت اعتبارسنجی که قبلاً توسط CsrfProtectionMiddleware ثبت شده است
var validationFeature = context.HttpContext.Features.Get<IAntiforgeryValidationFeature>();
if (validationFeature is { IsValid: false })
{
// رد کردن درخواست در صورت غیرمجاز بودن Cross-Site
context.Result = new AntiforgeryValidationFailedResult();
}
}
}هشدار امنیتی: دستکاری فوق یک راهکار آزمایشگاهی برای یکپارچهسازی کامل MVC است. در محیطهای عملیاتی (Production)، توصیه میشود تا انتشار نسخههای نهایی .NET 11 و پشتیبانی رسمی مایکروسافت، در MVC از رفتار پیشفرض (دفاع دو لایهای) استفاده کنید.
http://localhost:5173 در برابر https://localhost:5001).POST، PUT، PATCH یا DELETE هدر Sec-Fetch-Site: cross-site دریافت کرده و توسط لایه جدید CSRF رد میشوند.راهکار توسعه: اتکا به CORS در محیط محلی دیگر کافی نیست. سرورهای توسعه فرانتاند باید به گونهای پیکربندی شوند که درخواستهای API را پروکسی کنند تا ارتباط از دید مرورگر کاملاً Same-Origin به نظر برسد:
// نمونه پیکربندی در vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://localhost:5001',
changeOrigin: true,
secure: false
}
}
}
}fetch()، یک درخواست POST حاوی کد مجوز را به مسیر /connect/token ارسال کنند.Sec-Fetch-Site: cross-site را به آن الحاق میکند و سیستم جدید .NET 11 تبادل توکن را متوقف خواهد کرد.response_mode=form_post، ارائهدهنده خارجی توکن یا کد را به صورت یک فرم POST متقاطع به مسیر /signin-oidc برنامه شما ارسال میکند. نحوه برخورد میدلور با فیلد Sec-Fetch-Mode و ساختار ناوبری در این مرحله حیاتی است و نیازمند استثنا شدن این مسیرها در پایپلاین احراز هویت است.iframe به دامنههای مختلف کلاینتها باز میکند. با ترکیب Sec-Fetch-Dest: iframe و Sec-Fetch-Site: cross-site، امکان دارد اعلانهای خروج به صورت خاموش توسط سرورهای دریافتکننده رد شوند و وضعیت خروج کاربر با شکست مواجه گردد.Revocation Endpoint)Introspection Endpoint)┌─────────────────────────────────────────────────────────┐
│ مرورگر کاربر
│ ┌───────────────────────┐ Same-Origin POST / PUT
│ │ برنامه تکصفحهای │─────────────────────────┐
│ │ (SPA) │ │
│ └───────────────────────┘ │
└───────────────────────────────────────────────┼─────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ لایه میزبان BFF (Same-Origin)
│
│ - دریافت درخواستها در دامنه اصلی
│ - مدیریت کوکیهای فقط-سرور (HttpOnly Cookies)
│ - حذف اصطکاک هدرهای Sec-Fetch-Site
└────────────────────────────┬────────────────────────────┘
│
Server-to-Server (بدون هدر مرورگر)
│
▼
┌─────────────────────────────────────────────────────────┐
│ وبسرویسهای تجاری / سرویس احراز هویت (IdP)
└─────────────────────────────────────────────────────────┘Sec-Fetch-Site هستند (که در این حالت، فریمورک آنها را کلاینت غیر مرورگری فرض کرده و مجاز میداند).capacitor://localhost یا app:// ارسال میکنند که سبب ایجاد یک مقدار نامتعارف در هدر Origin و مغایرت با Host سرور میشود.نکته کلیدی: برنامههایی که بکاند .NET 11 دارند اما سرویسگیرنده آنها اپلیکیشنهای هیبریدی بر پایه WebView هستند، باید حتماً رفتار این کلاینتها را در ارسال هدرها بررسی کرده و در صورت رد درخواست، مبدأهای داخلی وبویو را در لیست دامنههای مجاز قرار دهند یا از پیادهسازی سفارشی ICsrfProtection استفاده کنند.HttpClient بدون سرصفحه یا تستهای مبتنی بر خط فرمان) هدر Sec-Fetch-Site را تولید نمیکنند و تمامی تستها در پایپلاین CI/CD موفقیتآمیز خواهند بود، اما کاربر نهایی در مرورگر با خطای ۴۰۰ مواجه میشود.{
"CrossOriginProtection": "disable"
}// مستثنیسازی مسیر تبادل توکن یا فرآیندهای بازگشتی در Minimal APIs
app.MapPost("/connect/token", HandleTokenExchange)
.DisableAntiforgery();