Claude
Skills
Sign in
Back

file-handling

Included with Lifetime
$97 forever

File uploads, streaming, storage abstractions, and secure file handling patterns for ASP.NET Core Razor Pages applications. Use when implementing secure file uploads in Razor Pages, streaming large files, or creating storage abstractions for file operations.

General

What this skill does


## Rationale

File handling is a common requirement but presents significant security and scalability challenges. Improper implementation can lead to security vulnerabilities (path traversal, malicious uploads), memory exhaustion, and storage inefficiencies. These patterns provide secure, performant, and maintainable approaches to file operations in Razor Pages.

## Patterns

### Pattern 1: Secure File Upload Validation

Implement comprehensive validation for file uploads including type, size, and content verification.

```csharp
public class FileUploadValidator
{
    private readonly long _maxFileSize;
    private readonly string[] _allowedExtensions;
    private readonly Dictionary<string, byte[]> _fileSignatures;

    public FileUploadValidator(IConfiguration configuration)
    {
        _maxFileSize = configuration.GetValue<long>("FileUpload:MaxSize", 10 * 1024 * 1024); // 10MB
        _allowedExtensions = configuration.GetSection("FileUpload:AllowedExtensions")
            .Get<string[]>() ?? new[] { ".jpg", ".jpeg", ".png", ".pdf", ".doc", ".docx" };
        
        // Magic numbers for file type validation
        _fileSignatures = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase)
        {
            [".jpg"] = new byte[] { 0xFF, 0xD8, 0xFF },
            [".jpeg"] = new byte[] { 0xFF, 0xD8, 0xFF },
            [".png"] = new byte[] { 0x89, 0x50, 0x4E, 0x47 },
            [".pdf"] = new byte[] { 0x25, 0x50, 0x44, 0x46 },
            [".docx"] = new byte[] { 0x50, 0x4B, 0x03, 0x04 }
        };
    }

    public async Task<ValidationResult> ValidateAsync(IFormFile file)
    {
        // Check file exists
        if (file == null || file.Length == 0)
        {
            return ValidationResult.Failure("No file provided");
        }

        // Check file size
        if (file.Length > _maxFileSize)
        {
            return ValidationResult.Failure(
                $"File size exceeds maximum allowed size of {_maxFileSize / 1024 / 1024}MB");
        }

        // Get and validate extension
        var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
        if (!_allowedExtensions.Contains(extension))
        {
            return ValidationResult.Failure(
                $"File type '{extension}' is not allowed. Allowed types: {string.Join(", ", _allowedExtensions)}");
        }

        // Validate file signature (magic number)
        if (_fileSignatures.TryGetValue(extension, out var signature))
        {
            using var stream = file.OpenReadStream();
            var header = new byte[signature.Length];
            var bytesRead = await stream.ReadAsync(header, 0, signature.Length);
            
            if (bytesRead < signature.Length || !header.SequenceEqual(signature))
            {
                return ValidationResult.Failure(
                    "File content does not match the declared file type");
            }
        }

        // Reset stream position if needed later
        if (file is { Position: > 0 })
        {
            file.Position = 0;
        }

        return ValidationResult.Success();
    }
}

public record ValidationResult(bool IsValid, string? ErrorMessage)
{
    public static ValidationResult Success() => new(true, null);
    public static ValidationResult Failure(string error) => new(false, error);
}
```

### Pattern 2: Streaming Large Files

Handle large file uploads efficiently without loading entire files into memory.

```csharp
public class StreamingFileUploadModel : PageModel
{
    private readonly IFileStorageService _storage;
    private readonly FileUploadValidator _validator;
    private readonly ILogger<StreamingFileUploadModel> _logger;

    public StreamingFileUploadModel(
        IFileStorageService storage,
        FileUploadValidator validator,
        ILogger<StreamingFileUploadModel> logger)
    {
        _storage = storage;
        _validator = validator;
        _logger = logger;
    }

    [BindProperty]
    public string? Description { get; set; }

    public string? ErrorMessage { get; set; }
    public string? SuccessMessage { get; set; }

    // Disable form value limit for streaming
    [RequestSizeLimit(500 * 1024 * 1024)] // 500MB
    [RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = 500 * 1024 * 1024)]
    public async Task<IActionResult> OnPostAsync()
    {
        if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
        {
            ErrorMessage = "Invalid content type";
            return Page();
        }

        var boundary = MultipartRequestHelper.GetBoundary(
            MediaTypeHeaderValue.Parse(Request.ContentType),
            int.MaxValue);
        
        var reader = new MultipartReader(boundary, HttpContext.Request.Body);
        var section = await reader.ReadNextSectionAsync();

        while (section != null)
        {
            if (ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition))
            {
                if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                {
                    var fileName = contentDisposition.FileName.Value?.Trim('"') ?? "unnamed";
                    var safeFileName = Path.GetFileName(fileName); // Prevent path traversal
                    
                    // Validate file type by extension
                    var extension = Path.GetExtension(safeFileName).ToLowerInvariant();
                    var allowedExtensions = new[] { ".pdf", ".doc", ".docx" };
                    
                    if (!allowedExtensions.Contains(extension))
                    {
                        ErrorMessage = $"File type '{extension}' not allowed";
                        return Page();
                    }

                    // Stream directly to storage without loading into memory
                    var fileId = await _storage.UploadStreamAsync(
                        section.Body, 
                        safeFileName, 
                        section.ContentType ?? "application/octet-stream");

                    SuccessMessage = $"File uploaded successfully with ID: {fileId}";
                    
                    // Log upload
                    _logger.LogInformation(
                        "File uploaded: {FileName} with ID {FileId} by user {User}",
                        safeFileName, fileId, User.Identity?.Name ?? "anonymous");
                }
            }

            section = await reader.ReadNextSectionAsync();
        }

        return Page();
    }
}

// Helper class
public static class MultipartRequestHelper
{
    public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
    {
        var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
        
        if (string.IsNullOrWhiteSpace(boundary))
        {
            throw new InvalidDataException("Missing content-type boundary");
        }

        if (boundary.Length > lengthLimit)
        {
            throw new InvalidDataException(
                $"Multipart boundary length limit {lengthLimit} exceeded");
        }

        return boundary;
    }

    public static bool IsMultipartContentType(string? contentType) =>
        !string.IsNullOrEmpty(contentType) && 
        contentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase);

    public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) =>
        contentDisposition != null &&
        contentDisposition.DispositionType.Equals("form-data") &&
        string.IsNullOrEmpty(contentDisposition.FileName.Value) &&
        string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);

    public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) =>
        contentDisposition != null &&
        contentDisposition.DispositionType.Equals("form-data") &&
        (!string.IsNullOr
Files: 1
Size: 23.9 KB
Complexity: 28/100
Category: General

Related in General