From a93a7e8bcaf151ada6196b44d93e0b97730d026b Mon Sep 17 00:00:00 2001 From: elenastamenkovska Date: Wed, 25 Mar 2026 15:56:45 +0100 Subject: [PATCH 1/5] feat: add supported extensions in appsettings --- Dappi.HeadlessCms/Models/AwsStorageOptions.cs | 3 ++ .../StorageServices/AwsS3StorageService.cs | 48 ++++++++++++------- Dappi.TestEnv/appsettings.json | 3 +- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Dappi.HeadlessCms/Models/AwsStorageOptions.cs b/Dappi.HeadlessCms/Models/AwsStorageOptions.cs index f942c21..444e5d2 100644 --- a/Dappi.HeadlessCms/Models/AwsStorageOptions.cs +++ b/Dappi.HeadlessCms/Models/AwsStorageOptions.cs @@ -4,4 +4,7 @@ public class AwsStorageOptions { public const string AwsStorage = "AWS:Storage"; public string? BucketName { get; set; } + public bool? UseCdn { get; set; } + public string? CdnUrl { get; set; } + public List? SupportedExtensions { get; set; } } diff --git a/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs b/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs index 989f159..a7c61e6 100644 --- a/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs +++ b/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs @@ -17,20 +17,24 @@ IS3ClientFactory factory ) : IMediaUploadService { private readonly IAmazonS3 _s3Client = factory.CreateClient(); + private readonly AwsStorageOptions _storageOptions = configuration + .GetSection(AwsStorageOptions.AwsStorage) + .Get()!; + private readonly AwsAccountOptions _accountOptions = configuration + .GetSection(AwsAccountOptions.AwsAccount) + .Get()!; public void DeleteMedia(MediaInfo media) { if (string.IsNullOrEmpty(media.Url)) return; - var bucketName = configuration["AWS:Storage:BucketName"]; - var uri = new Uri(media.Url); var objectKey = Path.GetFileName(uri.LocalPath); var deleteRequest = new DeleteObjectRequest { - BucketName = bucketName, + BucketName = _storageOptions.BucketName, Key = objectKey, }; @@ -49,24 +53,19 @@ public async Task SaveFileAsync(Guid mediaId, IFormFile file) public async Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndExtensionPair) { - var bucketName = configuration["AWS:Storage:BucketName"]; - var cdnUrl = configuration["AWS:Storage:CdnUrl"]; - var regionName = configuration["AWS:Account:Region"]; - - var useCdn = - bool.TryParse(configuration["AWS:Storage:UseCdn"], out var parsed) && parsed; - var extension = streamAndExtensionPair.Extension.StartsWith(".") ? streamAndExtensionPair.Extension : "." + streamAndExtensionPair.Extension; var objectKey = $"{mediaId}{extension}"; - var region = Amazon.RegionEndpoint.GetBySystemName(regionName ?? "eu-central-1"); + var region = Amazon.RegionEndpoint.GetBySystemName( + _accountOptions.Region ?? "eu-central-1" + ); var putRequest = new PutObjectRequest { - BucketName = bucketName, + BucketName = _storageOptions.BucketName, Key = objectKey, InputStream = streamAndExtensionPair.Stream, AutoCloseStream = true, @@ -74,9 +73,10 @@ public async Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndEx }; await _s3Client.PutObjectAsync(putRequest); - var baseUrl = useCdn - ? $"{cdnUrl}/{objectKey}" - : $"https://{bucketName}.s3.{region.SystemName}.amazonaws.com/{objectKey}"; + var baseUrl = + _storageOptions.UseCdn is not null && _storageOptions.UseCdn == true + ? $"{_storageOptions.CdnUrl}/{objectKey}" + : $"https://{_storageOptions.BucketName}.s3.{region.SystemName}.amazonaws.com/{objectKey}"; var media = await dbContext .DbContext.Set() @@ -96,7 +96,7 @@ public void ValidateFile(IFormFile file) var fileExtension = Path.GetExtension(file.FileName); - if (GetContentType(fileExtension) == "unsupported") + if (!IsExtensionSupported(fileExtension)) throw new Exception("Unsupported media type."); } @@ -122,6 +122,22 @@ private string GetContentType(string extension) => _ => "application/octet-stream", }; + private bool IsExtensionSupported(string extension) + { + if ( + _storageOptions.SupportedExtensions is null + || _storageOptions.SupportedExtensions.Count == 0 + ) + { + return true; + } + + var normalizedExtension = extension.TrimStart('.').ToLower(); + return _storageOptions.SupportedExtensions.Any(allowed => + allowed.Equals(normalizedExtension, StringComparison.CurrentCultureIgnoreCase) + ); + } + public override string ToString() => "aws-s3"; } } diff --git a/Dappi.TestEnv/appsettings.json b/Dappi.TestEnv/appsettings.json index 2866ba1..5894254 100644 --- a/Dappi.TestEnv/appsettings.json +++ b/Dappi.TestEnv/appsettings.json @@ -28,7 +28,8 @@ "Storage": { "UseCdn": false, "CdnUrl": "", - "BucketName": "" + "BucketName": "", + "SupportedExtensions": ["jpg", "pdf"] }, "Account": { "AccessKey": "", From 5d93991fe86f3e041ad9859375b9a2a2e7699ad6 Mon Sep 17 00:00:00 2001 From: elenastamenkovska Date: Wed, 1 Apr 2026 15:15:18 +0200 Subject: [PATCH 2/5] feat: remove redundant boolean in storage options --- Dappi.HeadlessCms/Models/AwsStorageOptions.cs | 1 - .../Services/StorageServices/AwsS3StorageService.cs | 7 +++---- Dappi.TestEnv/appsettings.json | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Dappi.HeadlessCms/Models/AwsStorageOptions.cs b/Dappi.HeadlessCms/Models/AwsStorageOptions.cs index 444e5d2..b50e27b 100644 --- a/Dappi.HeadlessCms/Models/AwsStorageOptions.cs +++ b/Dappi.HeadlessCms/Models/AwsStorageOptions.cs @@ -4,7 +4,6 @@ public class AwsStorageOptions { public const string AwsStorage = "AWS:Storage"; public string? BucketName { get; set; } - public bool? UseCdn { get; set; } public string? CdnUrl { get; set; } public List? SupportedExtensions { get; set; } } diff --git a/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs b/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs index a7c61e6..bf76f83 100644 --- a/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs +++ b/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs @@ -73,10 +73,9 @@ public async Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndEx }; await _s3Client.PutObjectAsync(putRequest); - var baseUrl = - _storageOptions.UseCdn is not null && _storageOptions.UseCdn == true - ? $"{_storageOptions.CdnUrl}/{objectKey}" - : $"https://{_storageOptions.BucketName}.s3.{region.SystemName}.amazonaws.com/{objectKey}"; + var baseUrl = !string.IsNullOrEmpty(_storageOptions.CdnUrl) + ? $"{_storageOptions.CdnUrl}/{objectKey}" + : $"https://{_storageOptions.BucketName}.s3.{region.SystemName}.amazonaws.com/{objectKey}"; var media = await dbContext .DbContext.Set() diff --git a/Dappi.TestEnv/appsettings.json b/Dappi.TestEnv/appsettings.json index 5894254..d1950bc 100644 --- a/Dappi.TestEnv/appsettings.json +++ b/Dappi.TestEnv/appsettings.json @@ -26,7 +26,6 @@ }, "AWS": { "Storage": { - "UseCdn": false, "CdnUrl": "", "BucketName": "", "SupportedExtensions": ["jpg", "pdf"] From ca6e3c63db5d0d65bfe64b247ffbe7b13a3fdff1 Mon Sep 17 00:00:00 2001 From: elenastamenkovska Date: Wed, 1 Apr 2026 16:17:42 +0200 Subject: [PATCH 3/5] feat: add fluent validation --- .../Validators/FileUploadRequestValidator.cs | 59 +++++++++++++++++++ .../Generators/ActionsGenerator.cs | 8 --- 2 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs diff --git a/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs b/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs new file mode 100644 index 0000000..92bfb83 --- /dev/null +++ b/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs @@ -0,0 +1,59 @@ +using Dappi.HeadlessCms.Models; +using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +namespace Dappi.HeadlessCms.Validators +{ + public class FileUploadRequest + { + public IFormFile File { get; set; } + public string FieldName { get; set; } + } + + public class FileUploadRequestValidator : AbstractValidator + { + private readonly AwsStorageOptions _storageOptions; + + public FileUploadRequestValidator(IConfiguration configuration) + { + _storageOptions = configuration + .GetSection(AwsStorageOptions.AwsStorage) + .Get()!; + + RuleFor(x => x.File) + .Cascade(CascadeMode.Stop) + .NotNull() + .WithMessage("No file was uploaded.") + .Must(file => file.Length > 0) + .WithMessage("File is empty.") + .Must(BeASupportedExtension) + .WithMessage("Unsupported media type."); + + RuleFor(x => x.FieldName).NotEmpty().WithMessage("Field name is required."); + } + + private bool BeASupportedExtension(IFormFile? file) + { + if (file == null) + return false; + + var extension = System.IO.Path.GetExtension(file.FileName); + if ( + _storageOptions.SupportedExtensions is null + || _storageOptions.SupportedExtensions.Count == 0 + ) + { + return true; + } + + var normalizedExtension = extension.TrimStart('.').ToLower(); + return _storageOptions.SupportedExtensions.Any(allowed => + allowed.Equals( + normalizedExtension, + System.StringComparison.CurrentCultureIgnoreCase + ) + ); + } + } +} diff --git a/Dappi.SourceGenerator/Generators/ActionsGenerator.cs b/Dappi.SourceGenerator/Generators/ActionsGenerator.cs index 1b3f41d..ec30e40 100644 --- a/Dappi.SourceGenerator/Generators/ActionsGenerator.cs +++ b/Dappi.SourceGenerator/Generators/ActionsGenerator.cs @@ -420,14 +420,6 @@ public async Task UploadFile(Guid id, IFormFile file, [FromForm] UploadDate = DateTime.UtcNow }; - try { - uploadService.ValidateFile(file); - } - catch(Exception ex) - { - return BadRequest(new {message = ex.Message}); - } - property.SetValue(entity, mediaInfo); await dbContext.Set().AddAsync(mediaInfo); From 6c3d047a7791edcdd5ff6cbed4420a9a92633541 Mon Sep 17 00:00:00 2001 From: elenastamenkovska Date: Thu, 2 Apr 2026 12:01:41 +0200 Subject: [PATCH 4/5] feat: remove validate file completely --- .../Interfaces/IMediaUploadService.cs | 31 ++- .../StorageServices/AwsS3StorageService.cs | 27 --- .../LocalStorageUploadService.cs | 224 +++++++++--------- 3 files changed, 124 insertions(+), 158 deletions(-) diff --git a/Dappi.HeadlessCms/Interfaces/IMediaUploadService.cs b/Dappi.HeadlessCms/Interfaces/IMediaUploadService.cs index 8004b29..c5c5db1 100644 --- a/Dappi.HeadlessCms/Interfaces/IMediaUploadService.cs +++ b/Dappi.HeadlessCms/Interfaces/IMediaUploadService.cs @@ -1,16 +1,15 @@ -using Dappi.HeadlessCms.Core.Requests; -using Dappi.HeadlessCms.Enums; -using Dappi.HeadlessCms.Models; -using Microsoft.AspNetCore.Http; - -namespace Dappi.HeadlessCms.Interfaces -{ - public interface IMediaUploadService - { - public void DeleteMedia(MediaInfo media); - Task UpdateStatusAsync(Guid mediaId, MediaUploadStatus status); - Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndExtensionPair); - Task SaveFileAsync(Guid mediaId, IFormFile file); - public void ValidateFile(IFormFile file); - } -} +using Dappi.HeadlessCms.Core.Requests; +using Dappi.HeadlessCms.Enums; +using Dappi.HeadlessCms.Models; +using Microsoft.AspNetCore.Http; + +namespace Dappi.HeadlessCms.Interfaces +{ + public interface IMediaUploadService + { + public void DeleteMedia(MediaInfo media); + Task UpdateStatusAsync(Guid mediaId, MediaUploadStatus status); + Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndExtensionPair); + Task SaveFileAsync(Guid mediaId, IFormFile file); + } +} diff --git a/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs b/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs index bf76f83..09c4a3a 100644 --- a/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs +++ b/Dappi.HeadlessCms/Services/StorageServices/AwsS3StorageService.cs @@ -88,17 +88,6 @@ public async Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndEx } } - public void ValidateFile(IFormFile file) - { - if (file == null || file.Length == 0) - throw new Exception("No file was uploaded."); - - var fileExtension = Path.GetExtension(file.FileName); - - if (!IsExtensionSupported(fileExtension)) - throw new Exception("Unsupported media type."); - } - public async Task UpdateStatusAsync(Guid mediaId, MediaUploadStatus status) { var media = await dbContext @@ -121,22 +110,6 @@ private string GetContentType(string extension) => _ => "application/octet-stream", }; - private bool IsExtensionSupported(string extension) - { - if ( - _storageOptions.SupportedExtensions is null - || _storageOptions.SupportedExtensions.Count == 0 - ) - { - return true; - } - - var normalizedExtension = extension.TrimStart('.').ToLower(); - return _storageOptions.SupportedExtensions.Any(allowed => - allowed.Equals(normalizedExtension, StringComparison.CurrentCultureIgnoreCase) - ); - } - public override string ToString() => "aws-s3"; } } diff --git a/Dappi.HeadlessCms/Services/StorageServices/LocalStorageUploadService.cs b/Dappi.HeadlessCms/Services/StorageServices/LocalStorageUploadService.cs index b16c486..28c3a10 100644 --- a/Dappi.HeadlessCms/Services/StorageServices/LocalStorageUploadService.cs +++ b/Dappi.HeadlessCms/Services/StorageServices/LocalStorageUploadService.cs @@ -1,115 +1,109 @@ -using System.Net.Mime; -using Dappi.HeadlessCms.Core.Requests; -using Dappi.HeadlessCms.Enums; -using Dappi.HeadlessCms.Interfaces; -using Dappi.HeadlessCms.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; - -namespace Dappi.HeadlessCms.Services.StorageServices -{ - public class LocalStorageUploadService(IDbContextAccessor dbContext) : IMediaUploadService - { - public void DeleteMedia(MediaInfo media) - { - if (media.Url == null) throw new ArgumentNullException(media.Url); - var filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", media.Url); - if (File.Exists(filePath)) File.Delete(filePath); - } - - public void ValidateFile(IFormFile file) - { - if (file == null || file.Length == 0) - throw new Exception("No file was uploaded."); - - var fileExtension = Path.GetExtension(file.FileName); - - if (GetContentType(fileExtension) == "unsupported") - throw new Exception("Unsupported media type."); - } - - public async Task SaveFileAsync(Guid mediaId, IFormFile file) - { - var uploadsFolder = Path.Combine( - Directory.GetCurrentDirectory(), - "wwwroot", - "uploads"); - - if (!Directory.Exists(uploadsFolder)) - Directory.CreateDirectory(uploadsFolder); - - var fileExtension = Path.GetExtension(file.FileName); - var fileName = $"{Guid.NewGuid()}_{mediaId}{fileExtension}"; - var filePath = Path.Combine(uploadsFolder, fileName); - - await using (var fileStream = new FileStream(filePath, FileMode.Create)) - { - await file.CopyToAsync(fileStream); - } - - var relativePath = $"uploads{Path.DirectorySeparatorChar}{fileName}"; - - var media = await dbContext.DbContext.Set() - .Where(m => m.Id == mediaId).FirstOrDefaultAsync(); - - if (media == null) return; - - media.Url = relativePath; - await dbContext.DbContext.SaveChangesAsync(); - } - - public async Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndExtensionPair) - { - var uploadsFolder = Path.Combine( - Directory.GetCurrentDirectory(), - "wwwroot", - "uploads"); - - if (!Directory.Exists(uploadsFolder)) - Directory.CreateDirectory(uploadsFolder); - - var fileName = $"{Guid.NewGuid()}_{mediaId}{streamAndExtensionPair.Extension}"; - var filePath = Path.Combine(uploadsFolder, fileName); - - await using (var fileStream = new FileStream(filePath, FileMode.Create)) - { - await streamAndExtensionPair.Stream.CopyToAsync(fileStream); - } - - var relativePath = $"uploads{Path.DirectorySeparatorChar}{fileName}"; - - var media = await dbContext.DbContext.Set() - .Where(m => m.Id == mediaId).FirstOrDefaultAsync(); - - if (media == null) return; - - media.Url = relativePath; - await dbContext.DbContext.SaveChangesAsync(); - } - - public async Task UpdateStatusAsync(Guid mediaId, MediaUploadStatus status) - { - var media = await dbContext.DbContext.Set() - .Where(m => m.Id == mediaId).FirstOrDefaultAsync(); - - if (media == null) return; - - media.Status = status; - await dbContext.DbContext.SaveChangesAsync(); - } - - private string GetContentType(string fileExtension) - { - return fileExtension.ToLower() switch - { - ".pdf" => MediaTypeNames.Application.Pdf, - ".jpg" or ".jpeg" => MediaTypeNames.Image.Jpeg, - ".png" => MediaTypeNames.Image.Png, - ".gif" => MediaTypeNames.Image.Gif, - _ => "unsupported", - }; - } - - public override string ToString() => "local"; - } -} \ No newline at end of file +using System.Net.Mime; +using Dappi.HeadlessCms.Core.Requests; +using Dappi.HeadlessCms.Enums; +using Dappi.HeadlessCms.Interfaces; +using Dappi.HeadlessCms.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Dappi.HeadlessCms.Services.StorageServices +{ + public class LocalStorageUploadService(IDbContextAccessor dbContext) : IMediaUploadService + { + public void DeleteMedia(MediaInfo media) + { + if (media.Url == null) + throw new ArgumentNullException(media.Url); + var filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", media.Url); + if (File.Exists(filePath)) + File.Delete(filePath); + } + + public async Task SaveFileAsync(Guid mediaId, IFormFile file) + { + var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads"); + + if (!Directory.Exists(uploadsFolder)) + Directory.CreateDirectory(uploadsFolder); + + var fileExtension = Path.GetExtension(file.FileName); + var fileName = $"{Guid.NewGuid()}_{mediaId}{fileExtension}"; + var filePath = Path.Combine(uploadsFolder, fileName); + + await using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(fileStream); + } + + var relativePath = $"uploads{Path.DirectorySeparatorChar}{fileName}"; + + var media = await dbContext + .DbContext.Set() + .Where(m => m.Id == mediaId) + .FirstOrDefaultAsync(); + + if (media == null) + return; + + media.Url = relativePath; + await dbContext.DbContext.SaveChangesAsync(); + } + + public async Task SaveFileAsync(Guid mediaId, StreamAndExtensionPair streamAndExtensionPair) + { + var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads"); + + if (!Directory.Exists(uploadsFolder)) + Directory.CreateDirectory(uploadsFolder); + + var fileName = $"{Guid.NewGuid()}_{mediaId}{streamAndExtensionPair.Extension}"; + var filePath = Path.Combine(uploadsFolder, fileName); + + await using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await streamAndExtensionPair.Stream.CopyToAsync(fileStream); + } + + var relativePath = $"uploads{Path.DirectorySeparatorChar}{fileName}"; + + var media = await dbContext + .DbContext.Set() + .Where(m => m.Id == mediaId) + .FirstOrDefaultAsync(); + + if (media == null) + return; + + media.Url = relativePath; + await dbContext.DbContext.SaveChangesAsync(); + } + + public async Task UpdateStatusAsync(Guid mediaId, MediaUploadStatus status) + { + var media = await dbContext + .DbContext.Set() + .Where(m => m.Id == mediaId) + .FirstOrDefaultAsync(); + + if (media == null) + return; + + media.Status = status; + await dbContext.DbContext.SaveChangesAsync(); + } + + private string GetContentType(string fileExtension) + { + return fileExtension.ToLower() switch + { + ".pdf" => MediaTypeNames.Application.Pdf, + ".jpg" or ".jpeg" => MediaTypeNames.Image.Jpeg, + ".png" => MediaTypeNames.Image.Png, + ".gif" => MediaTypeNames.Image.Gif, + _ => "unsupported", + }; + } + + public override string ToString() => "local"; + } +} From 2bb8fa22b5222414ad9aa00140e87b2f4fd95ac3 Mon Sep 17 00:00:00 2001 From: elenastamenkovska Date: Thu, 2 Apr 2026 12:09:34 +0200 Subject: [PATCH 5/5] feat: check for valid signature of file extensions --- .../Validators/FileUploadRequestValidator.cs | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs b/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs index 92bfb83..ab52ef8 100644 --- a/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs +++ b/Dappi.HeadlessCms/Validators/FileUploadRequestValidator.cs @@ -15,6 +15,50 @@ public class FileUploadRequestValidator : AbstractValidator { private readonly AwsStorageOptions _storageOptions; + private static readonly Dictionary> _fileSignatures = new() + { + { + "jpg", + new List { new byte[] { 0xFF, 0xD8, 0xFF } } + }, + { + "jpeg", + new List { new byte[] { 0xFF, 0xD8, 0xFF } } + }, + { + "png", + new List { new byte[] { 0x89, 0x50, 0x4E, 0x47 } } + }, + { + "gif", + new List { new byte[] { 0x47, 0x49, 0x46, 0x38 } } + }, + { + "bmp", + new List { new byte[] { 0x42, 0x4D } } + }, + { + "pdf", + new List { new byte[] { 0x25, 0x50, 0x44, 0x46 } } + }, + { + "doc", + new List { new byte[] { 0xD0, 0xCF, 0x11, 0xE0 } } + }, + { + "docx", + new List { new byte[] { 0x50, 0x4B, 0x03, 0x04 } } + }, + { + "xlsx", + new List { new byte[] { 0x50, 0x4B, 0x03, 0x04 } } + }, + { + "txt", + new List { new byte[] { 0xEF, 0xBB, 0xBF }, new byte[] { } } + }, + }; + public FileUploadRequestValidator(IConfiguration configuration) { _storageOptions = configuration @@ -27,33 +71,63 @@ public FileUploadRequestValidator(IConfiguration configuration) .WithMessage("No file was uploaded.") .Must(file => file.Length > 0) .WithMessage("File is empty.") - .Must(BeASupportedExtension) + .Must(BeAValidFile) .WithMessage("Unsupported media type."); RuleFor(x => x.FieldName).NotEmpty().WithMessage("Field name is required."); } - private bool BeASupportedExtension(IFormFile? file) + private bool BeAValidFile(IFormFile? file) { if (file == null) return false; - var extension = System.IO.Path.GetExtension(file.FileName); + if (!BeASupportedExtension(file)) + return false; + + return HasValidFileSignature(file); + } + + private bool BeASupportedExtension(IFormFile file) + { if ( - _storageOptions.SupportedExtensions is null + _storageOptions.SupportedExtensions == null || _storageOptions.SupportedExtensions.Count == 0 ) - { return true; - } - var normalizedExtension = extension.TrimStart('.').ToLower(); - return _storageOptions.SupportedExtensions.Any(allowed => - allowed.Equals( - normalizedExtension, - System.StringComparison.CurrentCultureIgnoreCase - ) + var extension = Path.GetExtension(file.FileName)?.TrimStart('.').ToLower(); + if (string.IsNullOrEmpty(extension)) + return false; + + return _storageOptions.SupportedExtensions.Any(ext => + ext.Equals(extension, System.StringComparison.InvariantCultureIgnoreCase) ); } + + private bool HasValidFileSignature(IFormFile file) + { + var extension = Path.GetExtension(file.FileName).TrimStart('.').ToLower(); + if (string.IsNullOrEmpty(extension) || !_fileSignatures.ContainsKey(extension)) + return false; + + var signatures = _fileSignatures[extension]; + + using var stream = file.OpenReadStream(); + foreach (var signature in signatures) + { + if (signature.Length == 0) + return true; + + var headerBytes = new byte[signature.Length]; + stream.Seek(0, SeekOrigin.Begin); + stream.ReadExactly(headerBytes, 0, headerBytes.Length); + + if (headerBytes.SequenceEqual(signature)) + return true; + } + + return false; + } } }