چند روز قبل، یکی از
کانالهای فنی معروف یوتیوب با بیش از 15 میلیون مشترک، هک و پاک شد! که داستان آنرا
در اینجا میتوانید پیگیری کنید. در این هک، مهاجم در سعی اول، پیشنهاد پشتیبانی مالی از شبکه را داده و در ایمیل دوم، پس از جلب اعتماد اولیه، یک فایل به ظاهر PDF مفاد قرارداد را ارسال کرده که با کلیک بر روی آن، تمام کوکیهای یوتیوب مالک کانال، سرقت و مورد سوء استفاده قرار گرفته! در یک چنین حالتی، مهم نیست که شما اعتبارسنجی دو مرحلهای را فعال کردهاید و یا از بهترین روشهای رمزنگاری برای امن کردن اطلاعات کوکی و یا توکن خود استفاده کردهاید، همینقدر که اصل محتوای کوکی و یا توکن شما در اختیار شخص دیگری قرار گیرد، میتواند بدون نیاز به لاگین و دانستن کلمهی عبور شما، بجای شما وارد سیستم شده و تغییرات دلخواهی را اعمال کند!
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه میتوانیم یک چنین حملاتی را مشکلتر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکنها و یا کوکیهای سرقت شده، در برنامههای مبتنی بر ASP.NET Core بررسی خواهیم کرد.
توسعهی یک سرویس تشخیص مرورگر و سیستم عامل شخص وارد شدهی به سیستم
یکی از روشهای غیرممکن کردن یک چنین حملاتی، درج مشخصات سیستم عامل و مرورگر شخص وارد شدهی به سیستم، در کوکی و همچنین توکن صادر شدهی حاصل از اعتبارسنجی موفق است. سپس زمانیکه قرار است از اطلاعات این کوکی و یا توکن در برنامه استفاده شود، این اطلاعات را با اطلاعات درخواست جاری کاربر مقایسه کرده و در صورت عدم تطابق، درخواست او را برگشت میزنیم. برای مثال اگر عملیات لاگین، در ویندوز انجام شده و اکنون توکن و یا کوکی حاصل، در سیستم عامل اندروید در حاصل استفادهاست، یعنی ... این عملیات مشکوک است و باید خاتمه یابد و کاربر باید مجبور به لاگین مجدد شود و نه اعتبارسنجی خودکار بدون زحمت!
برای این منظور میتوان از کتابخانهی
UA-Parser استفاده کرد و توسط آن سرویس زیر را توسعه داد:
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using UAParser;
namespace ASPNETCore2JwtAuthentication.Services;
/// <summary>
/// To invalidate an old user's token from a new device
/// </summary>
public class DeviceDetectionService : IDeviceDetectionService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISecurityService _securityService;
public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
{
_securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);
public string GetDeviceDetails(HttpContext context)
{
var ua = GetUserAgent(context);
if (ua is null)
{
return "unknown";
}
var client = Parser.GetDefault().Parse(ua);
var deviceInfo = client.Device.Family;
var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
//TODO: Add the user's IP address here, if it's a banking system.
return $"{deviceInfo}, {browserInfo}, {osInfo}";
}
public string GetDeviceDetailsHash(HttpContext context) =>
_securityService.GetSha256Hash(GetDeviceDetails(context));
public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);
public string GetCurrentUserTokenDeviceDetailsHash() =>
GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);
public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
{
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
return null;
}
return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
}
public bool HasCurrentUserTokenValidDeviceDetails() =>
HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);
public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
StringComparison.Ordinal);
private static string GetUserAgent(HttpContext context)
{
if (context is null)
{
return null;
}
return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
? userAgent.ToString()
: null;
}
}
توضیحات:
اصل کار این سرویس در متد زیر رخ میدهد:
public string GetDeviceDetails(HttpContext context)
{
var ua = GetUserAgent(context);
if (ua is null)
{
return "unknown";
}
var client = Parser.GetDefault().Parse(ua);
var deviceInfo = client.Device.Family;
var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
//TODO: Add the user's IP address here, if it's a banking system.
return $"{deviceInfo}, {browserInfo}, {osInfo}";
}
در اینجا با استفاده از کتابخانهی UA-Parser، سعی میکنیم تا جزئیات مرورگر و سیستم عامل شخص را تهیه کنیم. سپس در قسمت دیگری از این سرویس، این اطلاعات را هش میکنیم. از این جهت که هم حجم آن کاهش یابد و بیجهت کوکی و یا توکن ما را حجیم نکند و هم بررسی محتوای آن جهت شبیه سازی آن، غیرممکن شود. هر مشخصات دریافتی در حین لاگین، همواره یک هش مشخص و یکتا را دارد. به همین جهت متدهای هش کردن اطلاعات را هم در اینجا مشاهده میکنید. به علاوهی متد HasUserTokenValidDeviceDetails که کار آن، دریافت Claim مرتبط با این اطلاعات، از کوکی و یا توکن جاری و مقایسهی آن با اطلاعات Http Request جاری است. اگر این دو یکی نبودند، یعنی احتمال سوء استفادهی از اطلاعات شخص، وجود دارد.
اضافه کردن اطلاعات مشخصات دستگاه کاربر به کوکی و یا توکن او
همانطور که عنوان شد، در متد HasUserTokenValidDeviceDetails، ابتدا مشخصات دستگاه موجود در کوکی و یا توکن دریافتی، استخراج میشود. به همین جهت نیاز است این مشخصات را دقیقا در حین لاگین موفق، به صورت یک Claim جدید، برای مثال از نوع ClaimTypes.
System به مجموعهی Claims کاربر اضافه کرد:
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
ClaimValueTypes.String, _configuration.Value.Issuer),
یکپارچه کردن DeviceDetectionService با اعتبارسنجهای کوکیها و توکنها
پس از افزودن مشخصات سیستم کاربر وارد شدهی به سیستم، به صورت یک Claim جدید به توکنها، روش اعتبارسنجی اطلاعات موجود در توکن رسیده، در رخداد گردان OnTokenValidated است که امکان دسترسی به HttpContext و محتوای توکن را میسر میکند:
.AddJwtBearer(cfg =>
{
cfg.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
return tokenValidatorService.ValidateAsync(context);
},
};
});
و یا اگر از کوکیها استفاده میکنید، معادل آن به صورت زیر است:
.AddCookie(options =>
{
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
});
در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامهی تولید کنندهی JWT
در اینجا و برای یک برنامهی مبتنی بر کوکیها
در اینجا میتوانید مشاهده کنید.