عنوان:

‫چک‌لیست امنیت آپلود فایل در ASP.NET Core


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۱۲ ۱۸:۲۰
آدرس: www.dntips.ir
چکیده: امکان آپلود فایل یکی از ویژگی‌های کلیدی و در عین حال مخاطره‌آمیز در سامانه‌های وب مدرن از جمله سیستم‌های بانکی، فین‌تک، بورس و سامانه‌های سازمانی به شمار می‌رود. عدم رعایت استاندارهای امنیتی در پردازش فایل‌های دریافتی، برنامه‌های کاربردی را در معرض تهدیدات جدی نظیر اجرای از راه دور کد (RCE)، توزیع بدافزارها، حملات ازکاراندازی سرویس (DoS)، تزریق اسکریپت از طریق دامنه (Stored XSS) و افشای اطلاعات حساس قرار می‌دهد. این مقاله با تمرکز بر فریم‌ورک ASP.NET Core، ضمن کالبدشکافی انواع بردار‌های حمله مرتبط با آپلود فایل، راهکارهای عملی و کدنویسی امن بر پایه توصیه‌های OWASP را بررسی می‌کند. هدف این پژوهش، ارائه الگوی دفاع عمقی (Defense in Depth) جهت اطمینان از سلامت، اصالت و امنیت فایل‌های دریافتی در اپلیکیشن‌های Enterprise است.

۱. مقدمه
در عصر دیجیتال، تبادل فایل میان کاربران و سامانه‌ها اجتناب‌ناپذیر است. کاربران روزانه مدارک هویتی (KYC)، اسناد مالی، فاکتورها، تصاویر پروفایل و گزارش‌های متعددی را در قالب فرمت‌های PDF، PNG، Excel و غیره بارگذاری می‌کنند. اگرچه این قابلیت تجربه کاربری و کارایی تجاری را ارتقا می‌دهد، اما از منظر امنیت سایبری، یکی از پهناورترین سطوح حمله (Attack Surface) را ایجاد می‌کند. بسیاری از توسعه‌دهندگان تصوری ساده‌انگارانه از امنیت آپلود فایل دارند و اتکای خود را صرفاً بر بررسی پسوند فایل (Extension Check) یا نوع محتوای اعلام‌شده توسط مرورگر (MIME Type) محدود می‌کنند. این رویکرد به مهاجمان اجازه می‌دهد تا با شبیه‌سازی کدهای مخرب در قالب فایل‌های مجاز، ساختار امنیتی برنامه را به راحتی دور بزنند. در سامانه‌های حساس، یک باگ کوچک در بخش آپلود می‌تواند منجر به نفوذ به کل زیرساخت و دسترسی ناخواسته به سرور پایگاه‌داده گردد. از این رو، پیاده‌سازی مکانیزم‌های چندلایه جهت احراز اصالت فایل پیش از ذخیره‌سازی الزام‌آور است.

۲. تحلیل بردار‌های حمله در آپلود فایل (Attack Vectors)
برای طراحی یک سیستم دفاعی کارآمد، درک شیوه تفکر و تکنیک‌های مهاجمان ضروری است. شایع‌ترین روش‌های سوءاستفاده از سیستم‌های آپلود فایل عبارتند از:

۲.۱. پسوندهای دوگانه و چندگانه (Double Extensions)
مهاجمان با قرار دادن چند پسوند پیاپی سعی بر فریب دادن سرویس‌دهنده دارند (مانند invoice.pdf.aspx یا photo.php.png). در صورتی که سرور وب یا سرویس فایل از آخرین پسوند به درستی تحلیل نگند، ممکن است فایل را به عنوان یک اسکریپت اجرایی تفسیر و اجرا نماید.

۲.۲. دور زدن حروف بزرگ و کوچک (Case Sensitivity Bypass)
چنانچه سیستم فیلترینگ منطبق بر حروف کوچک/بزرگ حساس باشد، مهاجم با تغییر فرمت حروف (مانند shell.aSp یا malware.ASPX) از لیست سیاه عبور می‌کند، در حالی که سرور ویندوز (IIS) هردو حالت را یکسان دانسته و کد را اجرا می‌کند.

۲.۳. جعل هدر MIME Type (MIME Type Spoofing)
هدر Content-Type توسط مرورگر ارسال می‌شود و به‌راحتی با ابزارهایی مانند Burp Suite یا Postman قابل دستکاری است. فرستادن کدهای کلاینت‌سایدی یا شل‌های اجرایی تحت هدر image/png یکی از متداول‌ترین شیوه‌های دور زدن اعتبار سنجی‌های سطح پایین است.

۲.۴. نام‌گذاری مخرب و حمله پیمایش مسیر (Path Traversal & Malicious Filenames)
در صورتی که نام فایل بدون پالایش ذخیره شود، مهاجم می‌تواند با گنجاندن کاراکترهای کنترل مسیر (مانند ../../../../web.config) یا اسکریپت‌های مخرب (مانند .png) اقدام به بازنویسی فایل‌های حیاتی سیستم‌عامل یا حملات Stored XSS نماید.

۲.۵. حملات بمب زیپ (ZIP Bombs & Decompression Attacks)
فایل‌های فشرده آسیب‌پذیری خاصی ایجاد می‌کنند. یک فایل زیپ کوچک چند مگابایتی می‌تواند هنگام باز شدن (Extract) چندین ترابایت داده تولید کند و منجر به پر شدن فضای دیسک و حافظه رم، و در نتیجه ازکارافتادگی کامل سرور (DoS) شود.

۲.۶. کدهای مخرب نهفته در فایل‌های مجاز (Active Content in Static Files)
حتی اگر پسوند و امضای فایل درست باشد، اسناد PDF می‌توانند شامل JavaScript داینامیک باشند، فایل‌های Office دارای ماکروهای خطرساز VBA باشند و فایل‌های تصویر SVG محتوای اسکریپتی اجرا کنند.

۳. دستورالعمل جامع پیاده‌سازی امنیت در ASP.NET Core
جهت مقابله با تهدیدات فوق، باید الگوی دفاع لایه‌ای (Layered Defense) را پیاده‌سازی نمود. در ادامه، ۱۶ اصل کلیدی همراه با نمونه کدهای عملیاتی در ASP.NET Core ارائه شده است.

۱. محدودسازی پسوندها بر اساس لیست سفید (Allowlist Extension Validation)
هرگز از لیست سیاه (Blacklist) استفاده نکنید. فقط پسوندهای شناخته‌شده و مجاز را بر اساس نیازمندی کسب‌وکار مجاز بدارید. همچنین نام فایل و پسوند باید حتماً نرمال‌سازی (Normalize) شوند.

۲ و ۳. اعتبارسنجی هم‌زمان MIME Type و بایت‌های جادویی (Magic Bytes Validation)
هر نوع فایل دارای یک امضای دیجیتال بایتی منحصر‌به‌فرد در ابتدای فایل است. ارزیابی این امضا (File Signature / Magic Bytes) تضمین می‌کند که فایل تغییر هویت نداده است.

نوع فایلپسوند مجاز امضای جادویی (Hex Signature)توضیحات
PNG Image.png89 50 4E 47 0D 0A 1A 0Aامضای ثابت ۸ بایتی PNG
JPEG Image.jpg, .jpegFF D8 FFامضای ۳ بایتی JPEG
PDF Document.pdf25 50 44 46معادل عبارت %PDF در ASCII
ZIP Archive / OpenXML.zip, .docx, .xlsx50 4B 03 04امضای اصلی ساختار PKZIP

کد عملیاتی: اعتبارسنجی امضای فایل (Magic Bytes) در #C
public static class FileSecurityValidator
{
    private static readonly Dictionary<string, List<byte[]>> AllowedSignatures = 
        new Dictionary<string, List<byte[]>>
        {
            { ".png",  new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
            { ".jpg",  new List<byte[]> { new byte[] { 0xFF, 0xD8, 0xFF } } },
            { ".jpeg", new List<byte[]> { new byte[] { 0xFF, 0xD8, 0xFF } } },
            { ".pdf",  new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } }
        };

    public static async Task<bool> IsValidFileSignatureAsync(IFormFile file, string extension)
    {
        if (file == null || file.Length == 0) return false;
        if (!AllowedSignatures.ContainsKey(extension)) return false;

        using var stream = file.OpenReadStream();
        using var reader = new BinaryReader(stream);

        var maxSignatureSize = AllowedSignatures[extension].Max(s => s.Length);
        var headerBytes = reader.ReadBytes(maxSignatureSize);

        return AllowedSignatures[extension].Any(signature => 
            headerBytes.Take(signature.Length).SequenceEqual(signature));
    }
}

۴. بازرسی و پاک‌سازی محتوای درون فایل (Content Inspection & Sanitization)
شناسایی امضای اولیه به تنهایی کافی نیست. محتوای پویای اسناد باید بررسی یا حذف شوند. به عنوان مثال، در اسناد PDF می‌توان با ابزارهای جاوااسکریپت‌زدایی محتوا را یکبار رندر یا بازنویسی (Re-encode) کرد. در مورد تصاویر، بازنویسی مجدد تصویر (Resampling/Re-encoding) کلیه کدهای تزریق‌شده در متاداده‌های EXIF را پاکسازی می‌کند.

۵. اعمال محدودیت حجم فایل (Enforce File Size Limits)
در ASP.NET Core، حتماً حجم فایل ورودی را از طریق مشخصه [RequestSizeLimit] یا کانفیگ Kestrel محدود کنید تا از حملات DoS جلوگیری شود.
[HttpPost("upload")]
[RequestSizeLimit(10 * 1024 * 1024)] // محدودیت ۱۰ مگابایت
public async Task<IActionResult> UploadFile(IFormFile file)
{
    // منطق پردازش
    return Ok();
}

۶. تولید نام فایل امن (GUID/UUID Generation)
هرگز فایل را با نام اصلی آن بر روی دیسک ذخیره نکنید. نام فایل باید به یک Guid جدید تغییر یابد. نام اصلی فایل صرفاً به عنوان متاداده در پایگاه‌داده ذخیره شود.
string fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
string safeFileName = $"{Guid.NewGuid():N}{fileExtension}";

۷ و ۸. ذخیره‌سازی خارج از Application Server و خارج از Web Root
فایل‌ها نباید در پوشه wwwroot یا مسیرهای مستقیم وب‌سرور نگهداری شوند. بهترین روش، استفاده از سرویس‌های ذخیره‌سازی مستقل مانند Network Attached Storage (NAS)، Azure Blob Storage، Amazon S3 یا MinIO است.

مزیت معماری تفکیک دیسک:
جداسازی محل ذخیره‌سازی فایل از سرور اجراکننده برنامه، در صورت بروز نفوذ، مانع از دسترسی مستقیم مهاجم به فایل‌های پیکربندی و کدهای اجرایی برنامه می‌شود و قابلیت توسعه‌پذیری (Scalability) را افزایش می‌دهد.

۹. اسکن آنتی‌ویروس و قرنطینه‌سازی (Malware Scanning & Quarantine)
پیش از اینکه فایل در دسترس سایر کاربران قرار گیرد، باید توسط آنتی‌ویروس سرور یا APIهای پویش بدافزار (مانند ClamAV یا سرویس‌های ابری) اسکن شود. در این مدت فایل در وضعیت قرنطینه (Quarantine Status) باقی می‌ماند.

۱۰. محدودسازی دسترسی‌های فایل‌سیستم (Least Privilege Principle)
پوشه هدف ذخیره‌سازی باید فقط دارای دسترسی‌های Read/Write برای اکانت کاربر جاری اپلیکیشن (Application Pool / Worker Process) باشد. مجوز Execute (اجرا) باید قطعاً روی این پوشه‌ها غیرفعال گردد.

۱۱. رمزنگاری اسناد حساس در حالت سکون (Encryption at Rest)
اسناد حساس (مانند تصویر کارت ملی، گذرنامه و اسناد مالی) باید با الگوریتم‌های استاندارد (مانند AES-256) رمزنگاری شده و سپس روی دیسک قرار گیرند تا در صورت لو رفتن ذخیره‌ساز، داده‌ها افشا نشوند.

۱۲. مقابله با حملات فشرده‌سازی و بمب زیپ (ZIP Bomb Protection)
در صورت دریافت فایل‌های ZIP، پیش از بازکردن آن‌ها، نسبت ضریب فشرده‌سازی، تعداد فایل‌های داخل آرشیو و عمق پوشه‌ها را اعتبارسنجی کنید. فایل‌های دارای رمزعبور غیرقابل اسکن را رد کنید.

۱۳. اعمال نرخ محدودیت درخواست (Rate Limiting)
با استفاده از Rate Limiting Middleware در ASP.NET Core 7+ یا API Gateway، تعداد درخواست‌های آپلود به ازای هر IP / کاربر را محدود کنید.

۱۴. کنترل دسترسی و احراز هویت هنگام دانلود (Authorization Check)
دانلود فایل نباید از طریق URL مستقیم انجام شود. تمام درخواست‌های دانلود باید از یک Controller عبور کرده و سطوح دسترسی کاربر (ACL) بررسی شود.
[HttpGet("download/{fileId}")]
[Authorize]
public async Task<IActionResult> DownloadFile(Guid fileId)
{
    var fileMetadata = await _dbContext.Files.FindAsync(fileId);
    if (fileMetadata == null) return NotFound();

    // بررسی سطح دسترسی کاربر جاری
    if (!UserHasAccessToFile(User, fileMetadata))
        return Forbid();

    var fileStream = await _storageService.GetFileStreamAsync(fileMetadata.StoredFileName);
    return File(fileStream, fileMetadata.ContentType, fileMetadata.OriginalFileName);
}

۱۵. ثبت رویدادهای امنیتی (Comprehensive Logging & Audit Trail)
تمامی رویدادهای موفق و ناموفق آپلود و دانلود باید همراه با مشخصاتی نظیر User ID، IP Address، Timestamp، File Hash (SHA-256) و Correlation ID ثبت (Log) شوند.

۱۶. عدم اتکا به اعتبارسنجی سمت کلاینت (Server-Side Validation Only)
اعتبارسنجی JavaScript در مرورگر صرفاً برای بهبود تجربه کاربری (UX) است و به راحتی با ابزارهای تست نفوذ دور زده می‌شود. تمامی اعتبارسنجی‌ها الزماً باید در سمت سرور انجام گیرند.

چک‌لیست نهایی امنیت آپلود فایل (OWASP Security Checklist)
  • استفاده از پسوندهای مجاز (Allowlist) و نرمال‌سازی آن‌ها
  • بررسی و تطابق امضای بایتی فایل (Magic Bytes)
  • عدم اتکا به هدر Content-Type ارسال‌شده از مرورگر
  • تولید نام فایل با GUID و عدم ذخیره نام اصلی روی دیسک
  • ذخیره‌سازی خارج از پوشه wwwroot و سرور اصلی برنامه
  • غیرفعال‌سازی مجوز Execute روی پوشه ذخیره‌سازی
  • تعیین حداکثر حجم مجاز (Max File Size)
  • اسکن بدافزار و قرنطینه‌سازی پیش از انتشار
  • رمزنگاری فایل‌های حساس با الگوریتم AES-256
  • بررسی صلاحیت و احراز هویت در زمان دانلود فایل
  • ثبت تمامی تراکنش‌ها (Audit Logging) با Tracking ID

۴. نتیجه‌گیری
قابلیت آپلود فایل در سامانه‌های تحت وب نرم‌افزاری، فراتر از یک انتقال ساده داده است. هرفایل ورودی، بدون توجه به منبع ارسال (وب، اپلیکیشن موبایل یا API)، باید به عنوان ورودی ناشناخته و بالقوه خطرناک (Untrusted Input) تلقی گردد. استفاده ترکیبی از روش‌های اعتبار سنجی پسوند، امضای جادویی، جداسازی محل ذخیره‌سازی، محدودسازی دسترسی اجرایی، اسکن بدافزار و احراز هویت در دانلود، معماری مستحکمی را بر اساس موازین OWASP فراهم می‌سازد. رعایت این الزامات در برنامه‌های ASP.NET Core، سلامت زیرساخت و داده‌های کاربران را در برابر حملات متداول و پیشرفته تضمین می‌نماید.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۵/۱۸ ۰۹:۵۰
    بخش تکمیلی: معماری، پیکربندی زیرساخت و چرخه حیات فایل
    در مقاله پیشین به اصول امنیت در آپلود فایل بر اساس توصیه استانداردهای OWASP پرداخته شد. اما از دیدگاه یک مهندس ارشد، امنیت تنها یک لایه از طراحی است. لایه‌های دیگر شامل مدیریت منابع سرور، جلوگیری از فشار بر حافظه (RAM Exhaustion)، طراحی مدل وضعیت (State Machine) و پیکربندی سطح L7/Kestrel می‌باشد.

    ۱. لایه پیکربندی زیرساخت (Configuration Footgun Removal)
    پیش از آن‌که اولین بایت از کد آپلود اجرا شود، درخواست‌ها باید در لبه سرور (Edge/Kestrel) پالایش شوند. اگر فایلی حجم بیش از حد مجاز داشته باشد، Kestrel باید پیش از تخصیص حافظه و رفتن به Controller، درخواست را رد کند.

    کانفیگ Kestrel، فرم‌ها و Rate Limiter در ASP.NET Core:
    // Program.cs
    var builder = WebApplication.CreateBuilder(args);
    
    // ۱. تنظیم محدودیت‌های Kestrel در سطح سرور
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.Limits.MaxRequestBodySize = 524_288_000; // ۵۰۰ مگابایت حداکثر حجم Body
        options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10);
    });
    
    // ۲. تنظیم FormOptions جهت جلوگیری از مصرف بی‌رویه RAM
    builder.Services.Configure<FormOptions>(options =>
    {
        options.ValueLengthLimit = 1024 * 1024;         // ۱ مگابایت حداکثر برای هر Value در فرم
        options.MultipartBodyLengthLimit = 524_288_000; // ۵۰۰ مگابایت حداکثر حجم فایل
        options.MultipartHeadersLengthLimit = 32_768;   // ۳۲ کیلوبایت حداکثر هدر
        options.MemoryBufferThreshold = 64 * 1024;      // فایل‌های بالای ۶۴ کیلوبایت مستقیماً روی دیسک Buffer شوند
    });
    
    // ۳. اعمال محدودیت تعداد درخواست (Rate Limiting) بر اساس شناسه کاربر/IP
    builder.Services.AddRateLimiter(options =>
    {
        options.AddPolicy("UploadPolicy", context =>
        {
            var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
            return RateLimitPartition.GetFixedWindowLimiter(userId, _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 10,                 // مجاز: ۱۰ آپلود
                Window = TimeSpan.FromMinutes(1), // در هر دقیقه
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 2                    // حداکثر ۲ درخواست در صف
            });
        });
    });
    • نکته کلیدی MemoryBufferThreshold: تنظیم این مقدار روی 64KB باعث می‌شود فایل‌های بزرگ به‌جای بارگذاری کامل در RAM، روی دیسک موقت بافر شوند و از Crash کردن AppPool جلوگیری شود.

    ۲. مدل‌سازی متاداده و چرخه حیات فایل (File Entity & Lifecycle State Machine)
    فایل نباید بلافاصله پس از آپلود در دسترس قرار گیرد. یک سیستم تولیدی (Production-Ready) نیاز به تعریف ماشین وضعیت (State Machine) برای فایل دارد.
    public class UploadedFile
    {
        public Guid Id { get; set; }
        public string OriginalFileName { get; set; } = string.Empty;
        public string StoredFileName { get; set; } = string.Empty; // UUID
        public string ContentType { get; set; } = string.Empty;
        public string Extension { get; set; } = string.Empty;
        public long FileSize { get; set; }
        public string StoragePath { get; set; } = string.Empty; 
        public string StorageProvider { get; set; } = string.Empty; // e.g., "AzureBlob", "S3", "MinIO"
        
        public FileStatus Status { get; set; } = FileStatus.Pending;
        public string? ScanResult { get; set; }
        public DateTime? ScannedAt { get; set; }
        public string Checksum { get; set; } = string.Empty; // SHA-256 جهت بررسی یکپارچگی
        
        public string UploadedBy { get; set; } = string.Empty;
        public DateTime UploadedAt { get; set; } = DateTime.UtcNow;
        public DateTime? ExpiresAt { get; set; } // نگهداری بر اساس سیاست retention
    }
    
    public enum FileStatus
    {
        Pending,     // آپلود شده، در انتظار اسکن
        Scanning,    // در حال اسکن توسط آنتی‌ویروس
        Clean,       // اسکن موفق، آماده پردازش
        Infected,    // بدافزار شناسایی شد (قرنطینه)
        Processing,  // در حال پردازش (تولید Thumbnail یا Transcode)
        Ready,       // پردازش کامل و آماده دانلود
        Expired      // منقضی شده
    }

    ۳. سرویس اعتبارسنجی جامع (Complete Clean Validation Layer)
    ترکیب اعتبارسنجی حجم، پسوند، تطابق دقیق MIME Type بر اساس پسوند، بررسی Magic Bytes و پاک‌سازی نام فایل در یک سرویس واحد و منسجم:
    public interface IFileValidator
    {
        ValidationResult Validate(IFormFile file);
    }
    
    public sealed class FileValidator : IFileValidator
    {
        private readonly ILogger<FileValidator> _logger;
    
        private static readonly Dictionary<string, byte[]> FileSignatures = new(StringComparer.OrdinalIgnoreCase)
        {
            [".jpg"]  = new byte[] { 0xFF, 0xD8, 0xFF },
            [".jpeg"] = new byte[] { 0xFF, 0xD8, 0xFF },
            [".png"]  = new byte[] { 0x89, 0x50, 0x4E, 0x47 },
            [".gif"]  = new byte[] { 0x47, 0x49, 0x46, 0x38 },
            [".pdf"]  = new byte[] { 0x25, 0x50, 0x44, 0x46 },
            [".docx"] = new byte[] { 0x50, 0x4B, 0x03, 0x04 },
            [".xlsx"] = new byte[] { 0x50, 0x4B, 0x03, 0x04 },
            [".mp4"]  = new byte[] { 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70 }
        };
    
        private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
        {
            ".jpg", ".jpeg", ".png", ".gif", ".pdf", ".docx", ".xlsx", ".mp4"
        };
    
        private const long MaxFileSize = 500 * 1024 * 1024; // 500MB
        private const long MaxImageSize = 10 * 1024 * 1024; // 10MB
    
        public FileValidator(ILogger<FileValidator> logger) => _logger = logger;
    
        public ValidationResult Validate(IFormFile file)
        {
            var errors = new List<string>();
    
            // ۱. بررسی عدم خالی بودن و حجم کلی
            if (file == null || file.Length == 0)
                return ValidationResult.Failure(new[] { "فایل ارسال شده خالی است." });
    
            if (file.Length > MaxFileSize)
                errors.Add($"حجم فایل بیش از حد مجاز ({MaxFileSize / 1024 / 1024}MB) است.");
    
            // ۲. اعتبارسنجی پسوند
            var extension = Path.GetExtension(file.FileName);
            if (string.IsNullOrEmpty(extension) || !AllowedExtensions.Contains(extension))
            {
                _logger.LogWarning("تلاش برای آپلود پسوند غیرمجاز: {Extension}", extension);
                return ValidationResult.Failure(new[] { $"پسوند '{extension}' مجاز نیست." });
            }
    
            // محدودیت سخت‌گیرانه‌تر برای تصاویر
            if (IsImage(extension) && file.Length > MaxImageSize)
                errors.Add($"حجم تصاویر نباید بیش از {MaxImageSize / 1024 / 1024}MB باشد.");
    
            // ۳. تطابق MIME Type ادعا شده با پسوند
            var expectedMimeType = GetMimeTypeForExtension(extension);
            if (!string.Equals(file.ContentType, expectedMimeType, StringComparison.OrdinalIgnoreCase))
            {
                _logger.LogWarning("عدم تطابق MIME Type: انتظار می‌رفت {Expected} اما {Actual} دریافت شد.", expectedMimeType, file.ContentType);
                errors.Add("نوع Content-Type با پسوند فایل همخوانی ندارد.");
            }
    
            // ۴. اعتبارسنجی امضای بایتی (Magic Numbers)
            if (!ValidateMagicNumber(file, extension))
            {
                _logger.LogWarning("اعتبارسنجی امضای بایتی ناپایدار بود: {FileName}", file.FileName);
                errors.Add("محتوای واقعی فایل با پسوند آن همخوانی ندارد (احتمال Spoofing).");
            }
    
            return errors.Any() ? ValidationResult.Failure(errors) : ValidationResult.Success();
        }
    
        private static bool ValidateMagicNumber(IFormFile file, string extension)
        {
            if (!FileSignatures.TryGetValue(extension, out var signature))
                return false;
    
            using var stream = file.OpenReadStream();
            var header = new byte[signature.Length];
            var bytesRead = stream.Read(header, 0, signature.Length);
    
            return bytesRead == signature.Length && header.SequenceEqual(signature);
        }
    
        private static bool IsImage(string ext) => ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) ||
                                                    ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) ||
                                                    ext.Equals(".png", StringComparison.OrdinalIgnoreCase) ||
                                                    ext.Equals(".gif", StringComparison.OrdinalIgnoreCase);
    
        private static string GetMimeTypeForExtension(string ext) => ext.ToLowerInvariant() switch
        {
            ".jpg" or ".jpeg" => "image/jpeg",
            ".png" => "image/png",
            ".gif" => "image/gif",
            ".pdf" => "application/pdf",
            ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            ".mp4" => "video/mp4",
            _ => "application/octet-stream"
        };
    }
    
    public record ValidationResult(bool IsValid, IReadOnlyList<string> Errors)
    {
        public static ValidationResult Success() => new(true, Array.Empty<string>());
        public static ValidationResult Failure(IEnumerable<string> errors) => new(false, errors.ToList());
    }

    ۴. سه اصل کلیدی در معماری
    • هرگز به کلاینت اعتماد نکنید (Never Trust the Client): پسوندها، Content-Type و متاداده‌های کلاینت همگی قابل دستکاری هستند. تنها منبع حقیقت، بایت‌های اولیه فایل (Magic Bytes) و بازرسی سمت سرور است.
    • حافظه RAM بی‌نهایت نیست (Memory is Finite): خواندن مستقیم فایل‌های چند صد مگابایتی یا چند گیگابایتی در MemoryStream باعث Overload شدن GC و Crash کردن برنامه می‌شود. حتماً از Stream و Bufferهای محدود روی دیسک استفاده کنید.
    • تفکیک مسئولیت نگهداری و ارائه (Storage and Serving Separation): وب‌سرور شما (ASP.NET Core) وظیفه پردازش منطق و اعتبارسنجی را دارد، نه ذخیره‌سازی فایل روی دیسک خودش. فایل‌ها باید پس از اعتبارسنجی مستقیماً به سرویس‌های Obect Storage (مانند MinIO، S3 یا Azure Blob) منتقل شوند.