Security: Add MIME type validation and logging to ReceiptManager
- Add magic bytes validation to prevent file extension spoofing - Add ILogger for proper error logging in background parsing - Log background parsing success/failure with receipt ID 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -23,15 +23,37 @@ namespace MoneyMap.Services
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<ReceiptManager> _logger;
|
||||
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".pdf", ".gif", ".heic" };
|
||||
|
||||
public ReceiptManager(MoneyMapContext db, IWebHostEnvironment environment, IConfiguration configuration, IServiceProvider serviceProvider)
|
||||
// Magic bytes for file type validation (prevents extension spoofing)
|
||||
private static readonly Dictionary<string, byte[][]> FileSignatures = new()
|
||||
{
|
||||
{ ".jpg", new[] { new byte[] { 0xFF, 0xD8, 0xFF } } },
|
||||
{ ".jpeg", new[] { new byte[] { 0xFF, 0xD8, 0xFF } } },
|
||||
{ ".png", new[] { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
|
||||
{ ".gif", new[] { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }, // GIF87a or GIF89a
|
||||
{ ".pdf", new[] { new byte[] { 0x25, 0x50, 0x44, 0x46 } } }, // %PDF
|
||||
{ ".heic", new[] {
|
||||
new byte[] { 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63 }, // ftypheic
|
||||
new byte[] { 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63 }, // ftypheic (variant)
|
||||
new byte[] { 0x00, 0x00, 0x00 } // Generic ftyp header (relaxed check)
|
||||
}}
|
||||
};
|
||||
|
||||
public ReceiptManager(
|
||||
MoneyMapContext db,
|
||||
IWebHostEnvironment environment,
|
||||
IConfiguration configuration,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<ReceiptManager> logger)
|
||||
{
|
||||
_db = db;
|
||||
_environment = environment;
|
||||
_configuration = configuration;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private string GetReceiptsBasePath()
|
||||
@@ -69,6 +91,10 @@ namespace MoneyMap.Services
|
||||
if (!AllowedExtensions.Contains(extension))
|
||||
return ReceiptUploadResult.Failure($"File type {extension} not allowed. Use: {string.Join(", ", AllowedExtensions)}");
|
||||
|
||||
// Validate file content matches extension (magic bytes check)
|
||||
if (!await ValidateFileSignatureAsync(file, extension))
|
||||
return ReceiptUploadResult.Failure($"File content does not match {extension} format. The file may be corrupted or have an incorrect extension.");
|
||||
|
||||
// Create receipts directory if it doesn't exist
|
||||
var receiptsBasePath = GetReceiptsBasePath();
|
||||
if (!Directory.Exists(receiptsBasePath))
|
||||
@@ -133,10 +159,11 @@ namespace MoneyMap.Services
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var parser = scope.ServiceProvider.GetRequiredService<IReceiptParser>();
|
||||
await parser.ParseReceiptAsync(receipt.Id);
|
||||
_logger.LogInformation("Background parsing completed for receipt {ReceiptId}", receipt.Id);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail - upload was successful, parsing is optional
|
||||
_logger.LogError(ex, "Background parsing failed for receipt {ReceiptId}: {Message}", receipt.Id, ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -224,6 +251,23 @@ namespace MoneyMap.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<bool> ValidateFileSignatureAsync(IFormFile file, string extension)
|
||||
{
|
||||
if (!FileSignatures.TryGetValue(extension, out var signatures))
|
||||
return true; // No signature check for unknown extensions
|
||||
|
||||
var maxSignatureLength = signatures.Max(s => s.Length);
|
||||
var headerBytes = new byte[Math.Min(maxSignatureLength, (int)file.Length)];
|
||||
|
||||
await using var stream = file.OpenReadStream();
|
||||
_ = await stream.ReadAsync(headerBytes.AsMemory(0, headerBytes.Length));
|
||||
|
||||
// Check if file starts with any of the valid signatures for this extension
|
||||
return signatures.Any(signature =>
|
||||
headerBytes.Length >= signature.Length &&
|
||||
headerBytes.Take(signature.Length).SequenceEqual(signature));
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
|
||||
Reference in New Issue
Block a user