امنیت نشستها در عصر جدید وب: بررسی قابلیت آزمایشی DBSC در ASP.NET Core 11x
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۲۴ ۰۹:۳۵
آدرس: www.dntips.ir
چکیده: سرقت کوکیهای احراز هویت (Session Hijacking / Cookie Theft) بهویژه از طریق بدافزارهای سارق اطلاعات (Infostealers)، یکی از چالشهای بنیادین و پرخسارت در امنیت برنامههای تحت وب به شمار میرود. راهکار پیشنهادی وب نوین برای مقابله با این تهدید، استاندارد اعتبارنامههای نشست وابسته به دستگاه (Device Bound Session Credentials یا به اختصار DBSC) است. فریمورک ASP.NET Core در نسخه .NET 11 (با انتشار نسخه آزمایشی پکیج در RC1)، پیادهسازی سمت سرور این پروتکل را معرفی کرده است. در این مقاله، به بررسی مکانیسم امنیتی DBSC، معماری لایهای آن بر روی احراز هویت مبتنی بر کوکی، نحوه عملکرد اثبات مالکیت با کلید خصوصی سختافزاری، و پیادهسازی گامبهگام آن در ASP.NET Core میپردازیم.
HttpOnly و SameSite خطر حملاتی نظیر XSS و CSRF را کاهش میدهد، اما نمیتواند مانع سرقت فیزیکی یا آلودگیهای محلی از طریق تروجانها و Infostealerها شود.مزیت کلیدی امنیتی: حتی اگر مهاجم کوکی نشست را سرقت کند، چون کلید خصوصی متناظر روی دستگاه مهاجم وجود ندارد، امکان درخواست تمدید (Refresh) نشست را نخواهد داشت و به محض پایان عمر کوکی کوتاهمدت، دسترسی به طور کامل قطع میگردد.
<PackageReference Include="Microsoft.AspNetCore.Authentication.DeviceBoundSessions" Version="0.11.0-rc.1.26427.112" />
نکته وضعیت انتشار: این بسته در طول فاز توسعه .NET 11 آزمایشی (Experimental / Prerelease) خواهد ماند تا زمانی که خصوصیات پروتکل در مراجع وب تثبیت شود.
CookieAuthenticationScheme) قرار میگیرد. این میانافزار وظایف زیر را مدیریت میکند:Path-scoped refresh cookie).Short-Lived Cookies).Program.cs:var builder = WebApplication.CreateBuilder(args);
// افزودن سرویسهای احراز هویت و پیکربندی لایه DBSC
builder.Services
.AddAuthentication("Application")
.AddCookie("Application", options =>
{
// تنظیمات عمومی کوکی مانند مسیر لاگین
options.LoginPath = "/Account/Login";
})
.AddDeviceBoundSession("Application", options =>
{
// تعیین طول عمر کوکی نشست کوتاهمدت
options.ShortLivedCookieExpiration = TimeSpan.FromMinutes(10);
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();AddDeviceBoundSession با ارسال نام طرح احراز هویت کوکی ("Application")، لایه DBSC را به آن متصل میکند.ShortLivedCookieExpiration مدت اعتبار کوکی اصلیِ نشست را محدود میسازد (در این سناریو ۱۰ دقیقه). پس از پایان این بازه، مرورگر بهصورت پسزمینه و شفاف برای کاربر، عملیات تمدید امضاشده را اجرا میکند.chrome://flags) باشد.[Browser / Client] [Server]
| |
| ----- (1) Initial Login Request -----------------------> |
| <---- (2) Set-Cookie + Secure-Session-Registration ----- |
| |
| ----- (3) POST /dbsc/registration (JWT + Cookie) ------> |
| <---- (4) 200 OK + JSON Instructions + Short-Lived Cookie|
| |
| ... [Subsequent normal requests using Short-Lived Cookie] ...
| |
| ----- (5) POST /dbsc/refresh (Sec-Secure-Session-Id) --> |
| <---- (6) 403 Forbidden + Secure-Session-Challenge ----- |
| |
| ----- (7) POST /dbsc/refresh (Signed JWT Challenge) ---> |
| <---- (8) 200 OK + New Short-Lived Cookie -------------- |Secure-Session-Registration را ارسال میکند:HTTP/1.1 200 OK Secure-Session-Registration: (ES256 RS256); path="/dbsc/registration"; challenge="4a96e2cab06e76e2366b5e802bfcbabfe81e52c81abfcc71afc97010157ae9bd" Set-Cookie: MyAuthCookie=CfDJ8Apn9==; Path=/; SameSite=Lax; HttpOnly
(ES256 RS256): الگوریتمهای رمزنگاری امضایی که سرور برای مرورگر مجاز دانسته است (مانند ECDSA P-256 یا RSA).path: اندپوینتی که مرورگر باید برای ثبت نشست به آن درخواست بفرستد.challenge: یک رشته تصادفی (Nonce/Challenge) که مرورگر ملزم به امضای آن جهت اثبات تصاحب کلید است.challenge را با کلید خصوصی امضا میکند. سپس یک درخواست به اندپوینت ثبت با هدر Secure-Session-Response ارسال میکند که حاوی یک توکن متقارن JWT از نوع dbsc+jwt است:POST /dbsc/registration HTTP/1.1 Cookie: MyAuthCookie=CfDJ8Apn9== Secure-Session-Response: eyJhbGciOiJFUzI1NiIsImp3ayI6eyJ...
// Header
{
"alg": "ES256",
"jwk": {
"crv": "P-256",
"kty": "EC",
"x": "lHN3ar-wlVSSAdy8OJXqxhHwBiuur2rTlmbxcgim9_o",
"y": "zf_y79p8rpb7ztnQGZ25xPX_Lq_r7o1Ch9jvne70tJ4"
},
"typ": "dbsc+jwt"
}
// Payload
{
"jti": "4a96e2cab06e76e2366b5e802bfcbabfe81e52c81abfcc71afc97010157ae9bd"
}jwk) خود را برای سرور ارسال میکند و در بدنه (jti)، مقدار چالش دریافتی را قرار میدهد.HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: MyDbscCookie=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=300
{
"session_identifier": "199d6f60681",
"refresh_url": "/dbsc/refresh",
"scope": {
"origin": "https://example.com",
"include_site": false
},
"credentials": [
{
"type": "cookie",
"name": "MyDbscCookie",
"attributes": "Path=/; Secure; HttpOnly; SameSite=Lax"
}
]
}session_identifier: شناسه منحصربهفرد نشست DBSC که مرورگر باید در هر بار تمدید آن را ارائه دهد.refresh_url: آدرسی که مرورگر برای دریافت کوکی جدید کوتاهمدت باید فراخوانی کند.credentials: کوکی مورد نظر و مشخصات آن جهت اعمال در تراکنشها.POST /dbsc/refresh HTTP/1.1 Sec-Secure-Session-Id: 199d6f60681
403 Forbidden: سرور برای جلوگیری از Replay Attack و اطمینان از حضور کلاینت، درخواست را بلافاصله نمیپذیرد؛ بلکه یک خطای کنترلشده 403 همراه با چالش جدید برمیگرداند:HTTP/1.1 403 Forbidden Secure-Session-Challenge: "4524d32ab2b9"; id="199d6f60681"
POST /dbsc/refresh HTTP/1.1 Sec-Secure-Session-Id: 199d6f60681 Secure-Session-Response: <Signed-JWT-Containing-New-Challenge>
200 OK).تعویق درخواستها (Request Deferral): اگر عمر کوکی منقضی شده باشد و کاربر روی لینکی کلیک کند، مرورگر درخواست کاربر را در صف نگه میدارد (Defer میکند)، فرآیند پسزمینه تمدید را کامل کرده و سپس درخواست کاربر را با کوکی جدید ارسال میکند؛ بدون اینکه کاربر با لاگین مجدد یا خطای احراز هویت مواجه شود.
POST و مسیرهای خاص، برخی ابزارهای مسدودساز تبلیغات ممکن است به اشتباه درخواستهای تمدید نشست یا ثبت نشست را مسدود کنند.AddDeviceBoundSession برای مدیریت چرخه حیات نشست، تعامل سه نوع کوکی مجزا را سازماندهی میکند:HttpContext.SignInAsync که کلاینت را به فرآیند ثبت DBSC هدایت میکند.ShortLivedCookieExpiration (مثلاً ۱۰ دقیقه) محدود میشود./.well-known/dbsc/refresh ارسال میکند.Microsoft.AspNetCore.Authentication.DeviceBoundSessions کلید عمومی دستگاه کاربر را در دیتابیس یا کش توزیعشده ذخیره نمیکند.DbscPublicKeyJwk) به همراه شناسه نشست در ویژگیهای احراز هویت (AuthenticationProperties) قرار داده شده و داخل کوکی تمدید (Refresh Cookie) مهروموم (Encrypt) میشود:refreshProperties.Items["DbscPublicKeyJwk"] = jwtResult.PublicKeyJwk; refreshProperties.Items["DbscSessionId"] = sessionId; await Context.SignInAsync(Options.RefreshScheme, principal, refreshProperties);
challenge را در حافظه ذخیره نمیکند. در عوض، هر چالش به صورت یک توکن رمزنگاریشده با Data Protection تولید میشود که حاوی هویت کاربر، شناسه نشست و تاریخ انقضا است. بررسی چالش صرفاً با رمزگشایی مجدد و راستیآزمایی فیلدهای درون آن انجام میگیرد.403 Forbidden و ایجاد تأخیر در تجربه کاربری میشود. بازه پیشفرض ۱۰ دقیقه تعادلی بهینه میان امنیت و بار عملیاتی سختافزار است.jwk در هدر خود است، چرا که سرور کلید عمومی را از درون کوکیِ تمدید بازگشایی میکند.AddDefaultIdentity() یا AddIdentityCookies() زنجیره پیکربندی احراز هویت را در خود کپسوله میکند و شیء AuthenticationBuilder را مستقیماً برنمیگرداند. برای غلبه بر این محدودیت و اتصال لایه DBSC به کوکی پیشفرض Identity، میتوان از الگوی نمونهسازی دستی AuthenticationBuilder استفاده کرد:// ثبت پیشفرض Identity
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
// متصل کردن لایه DBSC به شمای کوکی پیشفرض Identity
new AuthenticationBuilder(builder.Services)
.AddDeviceBoundSession(sourceScheme: IdentityConstants.ApplicationScheme, options =>
{
options.ShortLivedCookieExpiration = TimeSpan.FromMinutes(10);
});AddDeviceBoundSession میتواند چندین بار با مقادیر مختلف فراخوانی شود؛ بنابراین در صورتی که سامانه از چند طرح کوکی مجزا استفاده کند، میتوان هرکدام را به طور مستقل به DBSC مجهز نمود.IdentityConstants.ApplicationScheme به DBSC ارجاع داده میشود، پکیج سه ساختار زیر را در پشت صحنه رجیستر میکند:[ Incoming Request ]
│
▼
[ PolicySchemeHandler: Identity.Application.Dbsc ]
│
├── آیا کوکی کوتاهمدت نشست در هدر موجود است؟
│ ├── بله ──► مسیریابی به [ Identity.Application.Dbsc.Session ]
│ └── خیر ──► عقبنشینی به [ Identity.Application ] (Source Scheme)
│
[ Refresh Scheme: Identity.Application.Dbsc.Refresh ] (محدود به مسیر /.well-known/dbsc)Identity.Application.Dbsc.Session): این طرح مسئول مدیریت کوکی احراز هویت اصلی با طول عمر کوتاه است (با پیشوند نام .AspNetCore.Identity.Application.Dbsc.Session).Identity.Application.Dbsc.Refresh): کوکی این طرح بهطور سختگیرانه به مسیر /.well-known/dbsc محدود (Path-scoped) شده و تنها در درخواستهای تمدید خوانده میشود.PolicySchemeHandler):PolicyScheme با عنوان {sourceScheme}.Dbsc تزریق میشود.ForwardDefaultSelector این طرح، وجود کوکی نشست کوتاهمدت را در درخواست ورودی جستجو میکند؛ در صورت یافتن نام کوکی، اعتبارسنجی را به طرح Session واگذار میکند و در غیر این صورت، درخواست را به طرح اولیه (Source Scheme) ارجاع میدهد تا خروج ناخواسته (Logout) رخ ندهد.IPostConfigureOptionsدر تزریق رفتارIPostConfigureOptions صورت میگیرد:PostConfigureDeviceBoundSessionCookieOptions: با زنجیرهسازی متد OnSigningIn، فراخوانی DeviceBoundSessionRegistrationHeader.Emit() را انجام میدهد تا هدر Secure-Session-Registration همراه با پاسخ لاگین ارسال شود. همچنین در رویداد خروج، حذف هر سه کوکی را برنامهریزی میکند.PostConfigureDeviceBoundSessionDerivedCookieOptions: پارامترهای پایهای امنیتی نظیر HttpOnly، Secure، SameSite و دامنه را از کوکی اصلی خوانده و روی کوکیهای نشست و تمدید اعمال مینماید.PostConfigureDeviceBoundSessionAuthenticationOptions: نام شمای پیشفرض سیستم احراز هویت را به شمای سیاستگذاری (PolicyScheme) جدید هدایت میکند./registration) و تمدید (/refresh) را در تب اصلی Network نمایش نمیدهد.Information تنظیم شود. جریان ثبت نشست اولیه در خروجی لاگ سرور به شرح زیر ثبت میگردد:info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/2 POST https://localhost:7029/.well-known/dbsc/registration - - 0
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[10]
AuthenticationScheme: Identity.Application.Dbsc.Refresh signed in.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[10]
AuthenticationScheme: Identity.Application.Dbsc.Session signed in.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[11]
AuthenticationScheme: Identity.Application signed out.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/2 POST https://localhost:7029/.well-known/dbsc/registration - 200 - application/jsonIdentity.Application signed out)/.well-known/dbsc را به دلیل شباهت رفتاری با ردیابها مسدود میکنند؛ از این رو در محیطهای تست، غیرفعالسازی موقت این افزونهها برای اطمینان از عملکرد پروتکل ضروری است.window.crypto.subtle استفاده کرد:// تولید جفتکلید ECDSA روی منحنی استاندارد P-256
const keyPair = await window.crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256"
},
false, // extractable: کلید خصوصی قابل اکسپورت نیست
["sign", "verify"]
);extractable = false: مرورگر دسترسی توابعی مانند exportKey را به کلید خصوصی مسدود میکند؛ در نتیجه کدهای جاوااسکریپت عادی نمیتوانند بایتهای خام کلید را استخراج کنند.extractable: false مانع از سرقت خود کلید میشود، اما اگر برنامه دچار آسیبپذیری Cross-Site Scripting (XSS) باشد، کد مخرب تزریقشده همچنان میتواند با فراخوانی متد crypto.subtle.sign() در همان کانتکست مرورگر، پیامهای دلخواه خود را توسط کلید قربانی امضا کند. بنابراین پیادهسازی لایههای دفاع در عمق مانند CSP و بهداشت کد کماکان الزامی است.r || s). این فرمت پیشفرض خروجی متد crypto.subtle.sign در مرورگرها است (برای منحنی P-256 طول آن دقیقا ۶۴ بایت است).System.Security.Cryptography.ECDsa در داتنت از طریق enum با عنوان DSASignatureFormat قابلیت اعتبارسنجی هر دو ساختار را فراهم میسازد:using System.Security.Cryptography;
using System.Text;
public class SignatureVerificationService
{
public bool VerifyClientSignature(string base64PublicKey, string rawMessage, string base64Signature)
{
byte[] publicKeyBytes = Convert.FromBase64String(base64PublicKey);
byte[] dataBytes = Encoding.UTF8.GetBytes(rawMessage);
byte[] signatureBytes = Convert.FromBase64String(base64Signature);
using var ecdsa = ECDsa.Create();
// ایمپورت کلید عمومی ارسالی از کلاینت (فرمت استاندارد SubjectPublicKeyInfo / SPKI)
ecdsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
// تلاش برای اعتبارسنجی با پشتیبانی از هر دو فرمت IEEE P1363 و ASN.1 DER
return TryVerify(ecdsa, dataBytes, signatureBytes, DSASignatureFormat.IeeeP1363FixedFieldConcatenation) ||
TryVerify(ecdsa, dataBytes, signatureBytes, DSASignatureFormat.Rfc3279DerSequence);
}
private static bool TryVerify(ECDsa ecdsa, byte[] data, byte[] signature, DSASignatureFormat format)
{
try
{
return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256, format);
}
catch (CryptographicException)
{
return false;
}
}
}"Hello") تنها صحت عملکرد الگوریتم را نشان میدهد اما در محیط عملیاتی فاقد امنیت است، زیرا همان امضای ثابت قابل شنود و ارسال مکرر (Replay Attack) خواهد بود. برای تبدیل این مکانیزم به یک پروتکل کامل امضای درخواست (HTTP Request Signing):crypto.subtle.sign داده میشود، باید ترکیبی هششده از مشخصات کلیدی درخواست باشد: DataToSign = HttpMethod + Path + Nonce + Timestamp + SHA256(RequestBody)