👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Improve Data Security by Validating File Contents in .NET

Improve Data Security by Validating File Contents in .NET

Author - Abdul Rahman (Bhai)

Security

9 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

You've secured your file storage paths—but what about the files themselves? A malicious user can disguise an executable as a PDF, upload a zip bomb, or inject viruses into your system. In this article, we'll explore how to validate file contents in .NET applications, covering file size limits, type validation, and antivirus scanning to protect your application from malicious uploads.

Don't trust the file extension alone. Let's learn how to verify what's really inside those uploaded files.

Why we gonna do?

File content validation is your second line of defense after securing file paths. Users can manipulate filenames, extensions, and content to bypass basic checks. Without proper validation, you risk accepting malicious files, exhausting server resources, or exposing your users to security threats.

The Hidden Dangers of File Uploads

Here's the reality: file extensions mean nothing. A user can rename malware.exe to invoice.pdf and upload it. Your application might store it, serve it back to users, or attempt to process it—creating serious security risks.

Consider these real-world threats:

  • File Type Spoofing: Attackers rename malicious executables with safe-looking extensions. If you only check the filename, you'll be fooled.
  • Zip Bombs: A tiny compressed file (kilobytes) that expands to gigabytes or terabytes when decompressed, filling up your disk space and crashing your service.
  • Resource Exhaustion: Massive file uploads consume disk space, bandwidth, and processing power, leading to degraded performance or outright denial of service.
  • Malware Distribution: If users can download files from your service, you become a distribution point for malware. Your reputation and legal liability are on the line.
  • Processing Exploits: Files with malicious content can exploit vulnerabilities in image processors, PDF readers, or document parsers running on your server.

Why File Extensions Aren't Enough

Every file type has a magic number or file signature—specific bytes at the beginning of the file that identify its true type. For example, PNG files always start with 89 50 4E 47, and JPEG files start with FF D8 FF.

If you rename a PDF to .xlsx and try to open it in Excel, Excel immediately knows it's not a real Excel file by checking those initial bytes. Your application should do the same.

The Cost of Not Validating

Without validation, you face multiple risks:

  • Storage Costs: In cloud environments, unlimited file uploads translate directly to higher bills. Large files consume storage and bandwidth.
  • Processing Time: Processing large files requires CPU resources and time. Multiply that by hundreds of users, and your service grinds to a halt.
  • Legal Liability: If your service hosts and distributes malware, you could face legal consequences and reputational damage.
  • Service Degradation: Disk space fills up, processing queues back up, and legitimate users suffer poor performance or outright failures.

Defense Requires Multiple Layers

No single check is perfect. Antivirus software is essential but not foolproof—different vendors detect different threats, and skilled attackers can create malware that evades detection. The only way to eliminate risk entirely is to not accept file uploads at all, but that's rarely practical.

Instead, we implement defense in depth: file size limits, magic number validation, and antivirus scanning working together to minimize risk.

How we gonna do?

Let's implement comprehensive file content validation in .NET, covering size limits, type verification, and malware scanning.

Step 1: Implement File Size Limits

The first and simplest defense is limiting how large files can be. This protects against resource exhaustion and controls costs. ASP.NET Core provides multiple ways to enforce size limits.

Application-Wide File Size Limit

Set a global default in your Program.cs:


// Configure global file upload limits
builder.Services.Configure<FormOptions>(options =>
{
    // Set maximum allowed size for multipart body (file uploads)
    options.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10 MB
    
    // Set maximum allowed size for in-memory buffering
    options.MemoryBufferThreshold = 2 * 1024 * 1024; // 2 MB
    
    // Maximum number of form fields allowed
    options.ValueCountLimit = 1024;
});
Per-Endpoint File Size Limits

For more granular control, use attributes on specific controllers or actions:


[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
    // Define constants for size limits
    private const int MaxFileSize = 5 * 1024 * 1024; // 5 MB
    private const int MaxRequestSize = 10 * 1024 * 1024; // 10 MB
    
    [HttpPost("upload")]
    [RequestFormLimits(
        MultipartBodyLengthLimit = MaxFileSize,
        ValueLengthLimit = MaxRequestSize
    )]
    [RequestSizeLimit(MaxRequestSize)]
    public async Task<IActionResult> Upload(IFormFile file)
    {
        // Validate file is provided
        if (file == null || file.Length == 0)
        {
            return BadRequest("No file uploaded");
        }
        
        // Additional validation: check file size explicitly
        if (file.Length > MaxFileSize)
        {
            return BadRequest($"File size exceeds maximum allowed size of " +
                            $"{MaxFileSize / 1024 / 1024} MB");
        }
        
        // Process the file...
        return Ok(new { FileName = file.FileName, Size = file.Length });
    }
}

RequestFormLimits controls the multipart form data size, while RequestSizeLimit limits the entire request. Using both provides comprehensive protection.

Step 2: Validate File Types Using Magic Numbers

Never trust file extensions. Instead, read the first few bytes of the file and verify they match the expected file signature or magic number. You can find lists of common file signatures online (e.g., Wikipedia's list of file signatures).

Create a File Validation Service

public class FileValidationService
{
    // Define allowed file signatures (magic numbers)
    private static readonly Dictionary<string, List<byte[]>> FileSignatures = 
        new(StringComparer.OrdinalIgnoreCase)
    {
        // PNG files start with: 89 50 4E 47 0D 0A 1A 0A
        { 
            ".png", 
            new List<byte[]> 
            { 
                new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } 
            } 
        },
        
        // JPEG files can start with: FF D8 FF E0 or FF D8 FF E1 or FF D8 FF E8
        { 
            ".jpg", 
            new List<byte[]> 
            { 
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }
            } 
        },
        { ".jpeg", new List<byte[]> 
            { 
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
                new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }
            } 
        },
        
        // PDF files start with: 25 50 44 46 (%PDF)
        { 
            ".pdf", 
            new List<byte[]> 
            { 
                new byte[] { 0x25, 0x50, 0x44, 0x46 } 
            } 
        },
        
        // ZIP files start with: 50 4B 03 04 or 50 4B 05 06 or 50 4B 07 08
        { 
            ".zip", 
            new List<byte[]> 
            { 
                new byte[] { 0x50, 0x4B, 0x03, 0x04 },
                new byte[] { 0x50, 0x4B, 0x05, 0x06 },
                new byte[] { 0x50, 0x4B, 0x07, 0x08 }
            } 
        }
    };
    
    public bool IsValidFileType(IFormFile file)
    {
        if (file == null || file.Length == 0)
        {
            return false;
        }
        
        // Get the file extension
        var extension = Path.GetExtension(file.FileName);
        
        if (string.IsNullOrEmpty(extension))
        {
            return false;
        }
        
        // Check if extension is in our allowed list
        if (!FileSignatures.ContainsKey(extension))
        {
            return false;
        }
        
        // Read the file signature (first 8 bytes is usually enough)
        using var reader = new BinaryReader(file.OpenReadStream());
        var headerBytes = reader.ReadBytes(8);
        
        // Get valid signatures for this extension
        var signatures = FileSignatures[extension];
        
        // Check if the file header matches any valid signature
        return signatures.Any(signature => 
            headerBytes.Take(signature.Length).SequenceEqual(signature));
    }
}
Use the Validation Service

[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
    private readonly FileValidationService _fileValidator;
    
    public FileUploadController(FileValidationService fileValidator)
    {
        _fileValidator = fileValidator;
    }
    
    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile file)
    {
        // Validate file type using magic numbers
        if (!_fileValidator.IsValidFileType(file))
        {
            return BadRequest("Invalid file type. Only PNG, JPEG, and PDF files " +
                            "are allowed.");
        }
        
        // Continue with other validations and processing...
        return Ok();
    }
}

Don't forget to register the service in your Program.cs:


builder.Services.AddScoped<FileValidationService>();

Step 3: Implement Antivirus Scanning

Even with file type validation, malicious content can hide inside legitimate file types. Antivirus scanning is essential for detecting malware, viruses, and other threats.

Using ClamAV for Virus Scanning

ClamAV is a free, open-source antivirus engine. While commercial solutions offer better detection rates, ClamAV is a solid choice for many applications.

First, install the NuGet package:


dotnet add package nClam

Set up ClamAV using Docker (easiest for development):


docker run -d -p 3310:3310 clamav/clamav
Create an Antivirus Scanning Service

using nClam;

public class AntivirusService
{
    private readonly ClamClient _clamClient;
    private readonly ILogger<AntivirusService> _logger;
    
    public AntivirusService(
        IConfiguration configuration, 
        ILogger<AntivirusService> logger)
    {
        var server = configuration["ClamAV:Server"] ?? "localhost";
        var port = int.Parse(configuration["ClamAV:Port"] ?? "3310");
        
        _clamClient = new ClamClient(server, port);
        _logger = logger;
    }
    
    public async Task<VirusScanResult> ScanFileAsync(IFormFile file)
    {
        try
        {
            // Convert file to byte array
            using var memoryStream = new MemoryStream();
            await file.CopyToAsync(memoryStream);
            var fileBytes = memoryStream.ToArray();
            
            // Scan the file
            var scanResult = await _clamClient.SendAndScanFileAsync(fileBytes);
            
            _logger.LogInformation(
                "Virus scan completed. File: {FileName}, Result: {Result}",
                file.FileName,
                scanResult.Result
            );
            
            return new VirusScanResult
            {
                IsClean = scanResult.Result == ClamScanResults.Clean,
                ResultMessage = scanResult.Result.ToString(),
                VirusName = scanResult.InfectedFiles?.FirstOrDefault()?.VirusName
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error scanning file: {FileName}", file.FileName);
            
            // Fail secure: if scanning fails, reject the file
            return new VirusScanResult
            {
                IsClean = false,
                ResultMessage = "Scan failed - file rejected for safety",
                VirusName = null
            };
        }
    }
}

public class VirusScanResult
{
    public bool IsClean { get; set; }
    public string ResultMessage { get; set; }
    public string? VirusName { get; set; }
}
Integrate Antivirus Scanning in Your Upload Endpoint

[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
    private readonly FileValidationService _fileValidator;
    private readonly AntivirusService _antivirusService;
    private readonly ILogger<FileUploadController> _logger;
    
    public FileUploadController(
        FileValidationService fileValidator,
        AntivirusService antivirusService,
        ILogger<FileUploadController> logger)
    {
        _fileValidator = fileValidator;
        _antivirusService = antivirusService;
        _logger = logger;
    }
    
    [HttpPost("upload")]
    [RequestSizeLimit(10 * 1024 * 1024)] // 10 MB
    public async Task<IActionResult> Upload(IFormFile file)
    {
        // 1. Basic validation
        if (file == null || file.Length == 0)
        {
            return BadRequest("No file uploaded");
        }
        
        // 2. Validate file type (efficient - check this first)
        if (!_fileValidator.IsValidFileType(file))
        {
            _logger.LogWarning(
                "Invalid file type rejected. FileName: {FileName}",
                file.FileName
            );
            return BadRequest("Invalid file type");
        }
        
        // 3. Scan for viruses (more expensive - do after type validation)
        var scanResult = await _antivirusService.ScanFileAsync(file);
        
        if (!scanResult.IsClean)
        {
            _logger.LogWarning(
                "Malicious file detected. FileName: {FileName}, Virus: {VirusName}",
                file.FileName,
                scanResult.VirusName
            );
            return BadRequest("File contains malicious content and was rejected");
        }
        
        // 4. File is safe - proceed with storage
        // ... save file logic here ...
        
        _logger.LogInformation(
            "File uploaded successfully. FileName: {FileName}, Size: {Size}",
            file.FileName,
            file.Length
        );
        
        return Ok(new 
        { 
            Message = "File uploaded successfully",
            FileName = file.FileName,
            Size = file.Length
        });
    }
}

Register the antivirus service in Program.cs:


builder.Services.AddScoped<AntivirusService>();

And add the configuration to appsettings.json:


{
  "ClamAV": {
    "Server": "localhost",
    "Port": "3310"
  }
}

Step 4: Testing with EICAR Test File

To verify your antivirus integration works, use the EICAR test file—a harmless file that all antivirus software treats as malware for testing purposes.

Create a text file with this content:


X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Important: Your antivirus software will flag this immediately. If you're in a corporate environment, inform your security team before creating this file. It's completely harmless but will trigger alerts.

Upload this file to your endpoint. You should see it rejected with a malicious content message.

Step 5: Additional Best Practices

Beyond the core validations, consider these additional security measures:

  • Sanitize Filenames: Remove special characters and path traversal attempts from filenames before storing (as covered in the previous article on safe file storage).
  • Implement Rate Limiting: Prevent abuse by limiting how many files a user can upload in a given time period.
  • Use Content-Type Validation: Check the Content-Type header, but don't rely on it alone—it's easily spoofed.
  • Quarantine Suspicious Files: Instead of rejecting borderline files immediately, quarantine them for manual review.
  • Multiple Antivirus Engines: For high-security applications, scan files with multiple antivirus engines. Services like VirusTotal offer APIs for this.
  • Regular Signature Updates: Ensure your antivirus engine regularly updates its virus definitions.
  • Monitor Upload Patterns: Track upload volumes, file types, and scan results to identify suspicious behavior.

Complete Example: Comprehensive File Upload Validation

Putting it all together, here's a complete example with all validation layers:


[ApiController]
[Route("api/[controller]")]
[Authorize] // Require authentication
public class SecureFileUploadController : ControllerBase
{
    private const int MaxFileSize = 10 * 1024 * 1024; // 10 MB
    private readonly FileValidationService _fileValidator;
    private readonly AntivirusService _antivirusService;
    private readonly ILogger<SecureFileUploadController> _logger;
    
    public SecureFileUploadController(
        FileValidationService fileValidator,
        AntivirusService antivirusService,
        ILogger<SecureFileUploadController> logger)
    {
        _fileValidator = fileValidator;
        _antivirusService = antivirusService;
        _logger = logger;
    }
    
    [HttpPost("upload")]
    [RequestFormLimits(MultipartBodyLengthLimit = MaxFileSize)]
    [RequestSizeLimit(MaxFileSize)]
    public async Task<IActionResult> Upload(
        [FromForm] IFormFile file,
        CancellationToken cancellationToken)
    {
        var uploadId = Guid.NewGuid();
        
        try
        {
            // Step 1: Basic validation
            if (file == null || file.Length == 0)
            {
                return BadRequest(new { Error = "No file provided" });
            }
            
            // Step 2: Size validation
            if (file.Length > MaxFileSize)
            {
                _logger.LogWarning(
                    "File size exceeded. UploadId: {UploadId}, Size: {Size}",
                    uploadId,
                    file.Length
                );
                return BadRequest(new 
                { 
                    Error = $"File size exceeds {MaxFileSize / 1024 / 1024} MB limit" 
                });
            }
            
            // Step 3: File type validation (fast - do this before expensive scanning)
            if (!_fileValidator.IsValidFileType(file))
            {
                _logger.LogWarning(
                    "Invalid file type. UploadId: {UploadId}, FileName: {FileName}",
                    uploadId,
                    file.FileName
                );
                return BadRequest(new 
                { 
                    Error = "Invalid file type. Only PNG, JPEG, and PDF files allowed" 
                });
            }
            
            // Step 4: Antivirus scanning (expensive - do last)
            var scanResult = await _antivirusService.ScanFileAsync(file);
            
            if (!scanResult.IsClean)
            {
                _logger.LogWarning(
                    "Malicious file detected. UploadId: {UploadId}, " +
                    "FileName: {FileName}, Virus: {VirusName}",
                    uploadId,
                    file.FileName,
                    scanResult.VirusName
                );
                return BadRequest(new 
                { 
                    Error = "File rejected: malicious content detected" 
                });
            }
            
            // Step 5: All validations passed - save the file
            var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
            var filePath = Path.Combine("uploads", userId, safeFileName);
            
            // Ensure directory exists
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            
            using (var stream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(stream, cancellationToken);
            }
            
            _logger.LogInformation(
                "File uploaded successfully. UploadId: {UploadId}, " +
                "FileName: {FileName}, StoredAs: {StoredFileName}",
                uploadId,
                file.FileName,
                safeFileName
            );
            
            return Ok(new
            {
                Message = "File uploaded successfully",
                FileName = safeFileName,
                OriginalFileName = file.FileName,
                Size = file.Length,
                UploadId = uploadId
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(
                ex,
                "Error processing file upload. UploadId: {UploadId}",
                uploadId
            );
            return StatusCode(500, new { Error = "Error processing file upload" });
        }
    }
}

Summary

Validating file contents requires multiple defensive layers working together. Implement file size limits to prevent resource exhaustion, validate file types using magic numbers to prevent spoofing, and perform antivirus scanning to detect malicious content.

Remember: file extensions are meaningless. Read the actual bytes. Antivirus isn't perfect, but it's essential. And no single validation is enough—defense in depth is the only way to protect your application and users.

Order matters too: validate file size first (cheapest), then file type (fast), and finally run antivirus scans (expensive). This efficient ordering minimizes resource waste while maintaining security.

The only way to eliminate file upload risk entirely is to not accept uploads at all. Since that's rarely an option, implement these comprehensive validations and monitor your system continuously for suspicious activity.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Security
  • File Validation
  • File Upload Security
  • Magic Numbers
  • File Signatures
  • Antivirus
  • ClamAV
  • File Size Limits
  • Malware Detection
  • Security
  • .NET