diff --git a/Directory.Version.props b/Directory.Version.props index 8d2acde..702302f 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,5 +1,5 @@ - 1.2.0 + 1.3.0 \ No newline at end of file diff --git a/README.md b/README.md index f3cfc66..88244f9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # ByteGuard File Validator ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.FileValidator) -**ByteGuard.FileValidator** is a lightweight security-focused library for validating user-supplied files in .NET applications. +**ByteGuard.FileValidator** is a lightweight security-focused library for validating user-supplied files in .NET applications. + It helps you enforce consistent file upload rules by checking: - Allowed file extensions - File size limits - File signatures (magic numbers) to detect spoofed types - Security validation for Office Open XML / Open Document Formats (`.docx`, `.xlsx`, `.pptx`, `.odt`, `.odp`, `.ods`) -- Malware scan result using a varity of scanners (_requires the addition of a specific ByteGuard.FileValidator scanner package_) +- Malware scan result using a varity of scanners (_requires the addition of a specific [ByteGuard.FileValidator](https://github.com/ByteGuard-HQ/byteguard-file-validator-net/wiki/Available-AV-scanners) scanner package_) > ⚠️ **Important:** This package is one layer in a defense-in-depth strategy. > It does **not** replace endpoint protection, sandboxing, input validation, or other security controls. @@ -18,9 +19,10 @@ It helps you enforce consistent file upload rules by checking: - ✅ Validate files by **size** - ✅ Validate files by **signature (_magic-numbers_)** - ✅ Validate **specification conformance** for archive-based formats (_Open XML and Open Document Formats_) -- ✅ **Ensure no malware** through a variety of antimalware scanners +- ✅ **Ensure no malware** through a variety of [antimalware scanners](https://github.com/ByteGuard-HQ/byteguard-file-validator-net/wiki/Available-AV-scanners) - ✅ Validate using file path, `Stream`, or `byte[]` -- ✅ Configure which file types to support +- ✅ [Rich integration with NET Core](https://github.com/ByteGuard-HQ/byteguard-file-validator-extensions-dependency-injection) +- ✅ Configure which [file types](https://github.com/ByteGuard-HQ/byteguard-file-validator-net/wiki/Validation-features) to support - ✅ Configure whether to **throw exceptions** or simply return a boolean - ✅ **Fluent configuration API** for easy setup @@ -38,7 +40,7 @@ dotnet add package ByteGuard.FileValidator ### Antimalware scanners -In order to use the antimalware scanning capabilities, ensure you have a ByteGuard.FileValidator antimalware package referenced as well. You can find the relevant scanner package on [NuGet](https://www.nuget.org/packages?q=ByteGuard.FileValidator.Scanner.&includeComputedFrameworks=true&prerel=true&sortby=relevance) under the namespace `ByteGuard.FileValidator.Scanner`. +In order to use the antimalware scanning capabilities, ensure you have a [ByteGuard.FileValidator antimalware package](https://github.com/ByteGuard-HQ/byteguard-file-validator-net/wiki/Available-AV-scanners) referenced as well. You can find the relevant scanner package on [NuGet](https://www.nuget.org/packages?q=ByteGuard.FileValidator.Scanner.&includeComputedFrameworks=true&prerel=true&sortby=relevance) under the namespace `ByteGuard.FileValidator.Scanner`. ## Usage @@ -130,38 +132,7 @@ public async Task Upload(IFormFile file) ## Supported File Extensions -The following file types are supported by the `FileValidator`: - -| Category | Supported extensions | -| ------------- | --------------------------------------------------------------------------------- | -| **Documents** | `.doc`, `.docx`, `.xls`, `.xlsx`, `.pptx`, `.odp`, `.ods`, `.odt`, `.pdf`, `.rtf` | -| **Images** | `.jpg`, `.jpeg`, `.png,`, `.bmp` | -| **Video** | `.mov`, `.avi`, `.mp4` | -| **Audio** | `.m4a`, `.mp3`, `.wav` | - -### Validation coverage per type - -`IsValidFile` always validates: - -- File extension (_against `SupportedFileTypes`_) -- File size (_against `FileSizeLimit`_) -- File signature (_magic number_) -- Malware scan result (_if an antimalware scanner has been configured_) - -For some formats, additional checks are performed: - -- **Microsoft Office / Open Document Format** (`.docx`, `.xlsx`, `.pptx`, `.ods`, `.odp`, `.odt`): - - Extension - - File size - - Signature - - Basic specification conformance validation - - Malware scan result - -- **Other binary formats**: - - Extension - - File size - - Signature - - Malware scan result +The file validator supports a variety of different file extensions. The complete list including the individual validation mechanisms per type, is available on the [WIKI](https://github.com/ByteGuard-HQ/byteguard-file-validator-net/wiki/Validation-features). ## Configuration Options diff --git a/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs b/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs index 88cfcba..bfca612 100644 --- a/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs +++ b/src/ByteGuard.FileValidator/Configuration/ConfigurationValidator.cs @@ -36,7 +36,7 @@ public static void ThrowIfInvalid(FileValidatorConfiguration configuration) } // Validate file type is supported by the current version of FileValidator. - if (!FileValidator.SupportedFileDefinitions.Any(f => f.FileType.Equals(fileType))) + if (!FileDefinitions.SupportedFileDefinitions.Any(f => f.FileType.Equals(fileType))) { throw new UnsupportedFileException($"File type '{fileType}' is not supported in the current version of FileValidator."); } diff --git a/src/ByteGuard.FileValidator/FileDefinitions.cs b/src/ByteGuard.FileValidator/FileDefinitions.cs new file mode 100644 index 0000000..34c6f16 --- /dev/null +++ b/src/ByteGuard.FileValidator/FileDefinitions.cs @@ -0,0 +1,316 @@ +using ByteGuard.FileValidator.Models; + +namespace ByteGuard.FileValidator; + +internal static class FileDefinitions +{ + /// + /// List of all supported valid file definitions, incl. their header signatures (magic numbers) + /// and potentially their corresponding valid subtype signatures. + /// + internal static readonly List SupportedFileDefinitions = new List + { + new FileDefinition + { + FileType = FileExtensions.Jpeg, + ValidSignatures = new List + { + new byte[] { 0xFF, 0xD8, 0xFF } // ÿØÿ + } + }, + new FileDefinition + { + FileType = FileExtensions.Jpg, + ValidSignatures = new List + { + new byte[] { 0xFF, 0xD8, 0xFF } // ÿØÿ + } + }, + new FileDefinition + { + FileType = FileExtensions.Jpe, + ValidSignatures = new List + { + new byte[] { 0xFF, 0xD8, 0xFF } // ÿØÿ + } + }, + new FileDefinition + { + FileType = FileExtensions.Pdf, + ValidSignatures = new List + { + new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D } // %PDF- + } + }, + new FileDefinition + { + FileType = FileExtensions.Png, + ValidSignatures = new List + { + new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } // ‰PNG␍␊␚␊ + } + }, + new FileDefinition + { + FileType = FileExtensions.Bmp, + ValidSignatures = new List + { + new byte[] { 0x42, 0x4D } // BM + } + }, + new FileDefinition + { + FileType = FileExtensions.Doc, + ValidSignatures = new List + { + new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 } // ÐÏ␑ࡱ␚á + } + }, + new FileDefinition + { + FileType = FileExtensions.Docx, + // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .docx. + ValidSignatures = new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ + } + }, + new FileDefinition + { + FileType = FileExtensions.Odp, + // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .odp. + ValidSignatures = new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ + } + }, + new FileDefinition + { + FileType = FileExtensions.Ods, + // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .ods. + ValidSignatures = new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ + } + }, + new FileDefinition + { + FileType = FileExtensions.Odt, + // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .odt. + ValidSignatures = new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ + } + }, + new FileDefinition + { + FileType = FileExtensions.Rtf, + ValidSignatures = new List + { + new byte[] { 0x7B, 0x5C, 0x72, 0x74, 0x66, 0x31 } // {\rtf1 + } + }, + new FileDefinition + { + FileType = FileExtensions.Xlsx, + // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .xlsx. + ValidSignatures = new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ + } + }, + new FileDefinition + { + FileType = FileExtensions.Xls, + ValidSignatures = new List + { + new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 } // ÐÏ␑ࡱ␚á + } + }, + new FileDefinition + { + FileType = FileExtensions.Pptx, + // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .pptx. + ValidSignatures = new List + { + new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ + } + }, + new FileDefinition + { + FileType = FileExtensions.M4a, + ValidSignatures = new List + { + new byte[] { 0x66, 0x74, 0x79, 0x70 } // ftyp + }, + SignatureOffset = 4, + HasSubtype = true, + SubtypeOffset = 8, + ValidSubtypeSignatures = new List + { + new byte[] { 0x4D, 0x34, 0x41, 0x20 } // M4A_ + } + }, + new FileDefinition + { + FileType = FileExtensions.Mov, + ValidSignatures = new List + { + new byte[] { 0x66, 0x74, 0x79, 0x70 } // ftyp + }, + SignatureOffset = 4, + HasSubtype = true, + SubtypeOffset = 8, + ValidSubtypeSignatures = new List + { + new byte[] { 0x71, 0x74, 0x20, 0x20 } // qt__ (Quicktime) + } + }, + new FileDefinition + { + FileType = FileExtensions.Avi, + ValidSignatures = new List + { + new byte[] { 0x52, 0x49, 0x46, 0x46 } // RIFF + }, + HasSubtype = true, + SubtypeOffset = 8, + ValidSubtypeSignatures = new List + { + new byte[] { 0x41, 0x56, 0x49, 0x20 } // AVI_ + } + }, + new FileDefinition + { + FileType = FileExtensions.Mp3, + ValidSignatures = new List + { + new byte[] { 0xFF, 0xFB }, // Without ID3 tag + new byte[] { 0xFF, 0xF2 }, // Without ID3 tag + new byte[] { 0xFF, 0xF3 }, // Without ID3 tag + new byte[] { 0x49, 0x44, 0x33 } // ID3 + } + }, + new FileDefinition + { + FileType = FileExtensions.Mp4, + ValidSignatures = new List + { + new byte[] { 0x66, 0x74, 0x79, 0x70 } // ftyp + }, + SignatureOffset = 4, + HasSubtype = true, + SubtypeOffset = 8, + ValidSubtypeSignatures = new List + { + new byte[] { 0x6D, 0x6D, 0x70, 0x34 }, // mmp4 (MP4) + new byte[] { 0x6D, 0x70, 0x34, 0x32 }, // mp42 (MP4 v2) + new byte[] { 0x69, 0x73, 0x6F, 0x6D }, // isom (ISO Base Media File) + new byte[] { 0x4D, 0x53, 0x4E, 0x56 } // MSNV (MPEG-4) + } + }, + new FileDefinition + { + FileType = FileExtensions.Wav, + ValidSignatures = new List + { + new byte[] { 0x52, 0x49, 0x46, 0x46 } // RIFF + }, + HasSubtype = true, + SubtypeOffset = 8, + ValidSubtypeSignatures = new List + { + new byte[] { 0x57, 0x41, 0x56, 0x45 } // WAVE + } + }, + new FileDefinition + { + FileType = FileExtensions.Txt, + AllowMissingSignature = true + }, + new FileDefinition + { + FileType = FileExtensions.Csv, + AllowMissingSignature = true + }, + new FileDefinition + { + FileType = FileExtensions.Ico, + ValidSignatures = new List + { + new byte[] { 0x00, 0x00, 0x01, 0x00 }, // ␀␀␁␀ + } + }, + new FileDefinition + { + FileType = FileExtensions.Heic, + ValidSignatures = new List + { + new byte[] { 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63 } // ftypheic + }, + SignatureOffset = 4 + }, + new FileDefinition + { + FileType = FileExtensions.Gif, + ValidSignatures = new List + { + new byte[] { 0x47, 0x49, 0x46, 0x38 }, // GIF8 + } + }, + new FileDefinition + { + FileType = FileExtensions.Tif, + ValidSignatures = new List + { + new byte[] { 0x49, 0x49, 0x2A, 0x00 }, // II*␀ (Intel byte order) + new byte[] { 0x4D, 0x4D, 0x00, 0x2A }, // MM␀* (Motorola byte order) + new byte[] { 0x49, 0x49, 0x2B, 0x00 }, // II+␀ (BigTIFF, Intel byte order) + new byte[] { 0x4D, 0x4D, 0x00, 0x2B } // MM␀+ (BigTIFF, Motorola byte order) + } + }, + new FileDefinition + { + FileType = FileExtensions.Tiff, + ValidSignatures = new List + { + new byte[] { 0x49, 0x49, 0x2A, 0x00 }, // II*␀ (Intel byte order) + new byte[] { 0x4D, 0x4D, 0x00, 0x2A }, // MM␀* (Motorola byte order) + new byte[] { 0x49, 0x49, 0x2B, 0x00 }, // II+␀ (BigTIFF, Intel byte order) + new byte[] { 0x4D, 0x4D, 0x00, 0x2B } // MM␀+ (BigTIFF, Motorola byte order) + } + }, + new FileDefinition + { + FileType = FileExtensions.Ogg, + ValidSignatures = new List + { + new byte[] { 0x4F, 0x67, 0x67, 0x53 } // OggS + } + }, + new FileDefinition + { + FileType = FileExtensions.Oga, + ValidSignatures = new List + { + new byte[] { 0x4F, 0x67, 0x67, 0x53 } // OggS + } + }, + new FileDefinition + { + FileType = FileExtensions.Ogv, + ValidSignatures = new List + { + new byte[] { 0x4F, 0x67, 0x67, 0x53 } // OggS + } + }, + new FileDefinition + { + FileType = FileExtensions.Ogx, + ValidSignatures = new List + { + new byte[] { 0x4F, 0x67, 0x67, 0x53 } // OggS + } + } + }; +} diff --git a/src/ByteGuard.FileValidator/FileExtensions.cs b/src/ByteGuard.FileValidator/FileExtensions.cs index fbbdee1..e48fa49 100644 --- a/src/ByteGuard.FileValidator/FileExtensions.cs +++ b/src/ByteGuard.FileValidator/FileExtensions.cs @@ -109,5 +109,60 @@ public static class FileExtensions /// Waveform Audio File Format. /// public const string Wav = ".wav"; + + /// + /// Text File. + /// + public const string Txt = ".txt"; + + /// + /// Comma-Separated Values. + /// + public const string Csv = ".csv"; + + /// + /// Icon File. + /// + public const string Ico = ".ico"; + + /// + /// High Efficiency Image Container. + /// + public const string Heic = ".heic"; + + /// + /// Graphics Interchange Format. + /// + public const string Gif = ".gif"; + + /// + /// Tag Image File Format. + /// + public const string Tif = ".tif"; + + /// + /// Tag Image File Format. + /// + public const string Tiff = ".tiff"; + + /// + /// Ogg Vorbis Audio. + /// + public const string Ogg = ".ogg"; + + /// + /// Ogg Vorbis Audio. + /// + public const string Oga = ".oga"; + + /// + /// Ogg Video. + /// + public const string Ogv = ".ogv"; + + /// + /// Ogg Multiplex. + /// + public const string Ogx = ".ogx"; } } diff --git a/src/ByteGuard.FileValidator/FileValidator.cs b/src/ByteGuard.FileValidator/FileValidator.cs index 1451ed3..77333c0 100644 --- a/src/ByteGuard.FileValidator/FileValidator.cs +++ b/src/ByteGuard.FileValidator/FileValidator.cs @@ -1,7 +1,6 @@ using DocumentFormat.OpenXml.Packaging; using ByteGuard.FileValidator.Configuration; using ByteGuard.FileValidator.Exceptions; -using ByteGuard.FileValidator.Models; using ByteGuard.FileValidator.Validators; using ByteGuard.FileValidator.Scanners; @@ -12,227 +11,6 @@ namespace ByteGuard.FileValidator /// public class FileValidator { - /// - /// List of all supported valid file definitions, incl. their header signatures (magic numbers) - /// and potentially their corresponding valid subtype signatures. - /// - internal static readonly List SupportedFileDefinitions = new List - { - new FileDefinition - { - FileType = FileExtensions.Jpeg, - ValidSignatures = new List - { - new byte[] { 0xFF, 0xD8, 0xFF } // ÿØÿ - } - }, - new FileDefinition - { - FileType = FileExtensions.Jpg, - ValidSignatures = new List - { - new byte[] { 0xFF, 0xD8, 0xFF } // ÿØÿ - } - }, - new FileDefinition - { - FileType = FileExtensions.Jpe, - ValidSignatures = new List - { - new byte[] { 0xFF, 0xD8, 0xFF } // ÿØÿ - } - }, - new FileDefinition - { - FileType = FileExtensions.Pdf, - ValidSignatures = new List - { - new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D } // %PDF- - } - }, - new FileDefinition - { - FileType = FileExtensions.Png, - ValidSignatures = new List - { - new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } // ‰PNG␍␊␚␊ - } - }, - new FileDefinition - { - FileType = FileExtensions.Bmp, - ValidSignatures = new List - { - new byte[] { 0x42, 0x4D } // BM - } - }, - new FileDefinition - { - FileType = FileExtensions.Doc, - ValidSignatures = new List - { - new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 } // ÐÏ␑ࡱ␚á - } - }, - new FileDefinition - { - FileType = FileExtensions.Docx, - // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .docx. - ValidSignatures = new List - { - new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ - } - }, - new FileDefinition - { - FileType = FileExtensions.Odp, - // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .odp. - ValidSignatures = new List - { - new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ - } - }, - new FileDefinition - { - FileType = FileExtensions.Ods, - // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .ods. - ValidSignatures = new List - { - new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ - } - }, - new FileDefinition - { - FileType = FileExtensions.Odt, - // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .odt. - ValidSignatures = new List - { - new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ - } - }, - new FileDefinition - { - FileType = FileExtensions.Rtf, - ValidSignatures = new List - { - new byte[] { 0x7B, 0x5C, 0x72, 0x74, 0x66, 0x31 } // {\rtf1 - } - }, - new FileDefinition - { - FileType = FileExtensions.Xlsx, - // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .xlsx. - ValidSignatures = new List - { - new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ - } - }, - new FileDefinition - { - FileType = FileExtensions.Xls, - ValidSignatures = new List - { - new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 } // ÐÏ␑ࡱ␚á - } - }, - new FileDefinition - { - FileType = FileExtensions.Pptx, - // WARNING: This shares the same signature as .zip and could potentially allow for .zip disguised as .pptx. - ValidSignatures = new List - { - new byte[] { 0x50, 0x4B, 0x03, 0x04 } // PK␃␄ - } - }, - new FileDefinition - { - FileType = FileExtensions.M4a, - ValidSignatures = new List - { - new byte[] { 0x66, 0x74, 0x79, 0x70 } // ftyp - }, - SignatureOffset = 4, - HasSubtype = true, - SubtypeOffset = 8, - ValidSubtypeSignatures = new List - { - new byte[] { 0x4D, 0x34, 0x41, 0x20 } // M4A_ - } - }, - new FileDefinition - { - FileType = FileExtensions.Mov, - ValidSignatures = new List - { - new byte[] { 0x66, 0x74, 0x79, 0x70 } // ftyp - }, - SignatureOffset = 4, - HasSubtype = true, - SubtypeOffset = 8, - ValidSubtypeSignatures = new List - { - new byte[] { 0x71, 0x74, 0x20, 0x20 } // qt__ (Quicktime) - } - }, - new FileDefinition - { - FileType = FileExtensions.Avi, - ValidSignatures = new List - { - new byte[] { 0x52, 0x49, 0x46, 0x46 } // RIFF - }, - HasSubtype = true, - SubtypeOffset = 8, - ValidSubtypeSignatures = new List - { - new byte[] { 0x41, 0x56, 0x49, 0x20 } // AVI_ - } - }, - new FileDefinition - { - FileType = FileExtensions.Mp3, - ValidSignatures = new List - { - new byte[] { 0xFF, 0xFB }, // Without ID3 tag - new byte[] { 0xFF, 0xF2 }, // Without ID3 tag - new byte[] { 0xFF, 0xF3 }, // Without ID3 tag - new byte[] { 0x49, 0x44, 0x33 } // ID3 - } - }, - new FileDefinition - { - FileType = FileExtensions.Mp4, - ValidSignatures = new List - { - new byte[] { 0x66, 0x74, 0x79, 0x70 } // ftyp - }, - SignatureOffset = 4, - HasSubtype = true, - SubtypeOffset = 8, - ValidSubtypeSignatures = new List - { - new byte[] { 0x6D, 0x6D, 0x70, 0x34 }, // mmp4 (MP4) - new byte[] { 0x6D, 0x70, 0x34, 0x32 }, // mp42 (MP4 v2) - new byte[] { 0x69, 0x73, 0x6F, 0x6D }, // isom (ISO Base Media File) - new byte[] { 0x4D, 0x53, 0x4E, 0x56 } // MSNV (MPEG-4) - } - }, - new FileDefinition - { - FileType = FileExtensions.Wav, - ValidSignatures = new List - { - new byte[] { 0x52, 0x49, 0x46, 0x46 } // RIFF - }, - HasSubtype = true, - SubtypeOffset = 8, - ValidSubtypeSignatures = new List - { - new byte[] { 0x57, 0x41, 0x56, 0x45 } // WAVE - } - } - }; - /// /// Specific file extensions for files that are Open XML documents. /// These require extra care when validating, as Open XML files are ZIP-archives and can contain potentially harmful content. @@ -454,7 +232,7 @@ public bool IsValidFileType(string fileName) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); var isSupported = _configuration.SupportedFileTypes.Contains(extension, StringComparer.InvariantCultureIgnoreCase) && - SupportedFileDefinitions.Any(fd => fd.FileType.Equals(extension, StringComparison.InvariantCultureIgnoreCase)); + FileDefinitions.SupportedFileDefinitions.Any(fd => fd.FileType.Equals(extension, StringComparison.InvariantCultureIgnoreCase)); if (!isSupported && _configuration.ThrowExceptionOnInvalidFile) { @@ -509,7 +287,7 @@ public bool HasValidSignature(string fileName, Stream stream) throw new InvalidOperationException("Stream is not seekable."); } - var fileDefinition = SupportedFileDefinitions.FirstOrDefault(fd => + var fileDefinition = FileDefinitions.SupportedFileDefinitions.FirstOrDefault(fd => fd.FileType.Equals(extension, StringComparison.InvariantCultureIgnoreCase)); if (fileDefinition == null) @@ -522,6 +300,12 @@ public bool HasValidSignature(string fileName, Stream stream) return false; } + // If the file definition allows for missing signatures, we can return early as the signature validation is effectively bypassed. + if (fileDefinition.AllowMissingSignature) + { + return true; + } + // As PDF documents are somewhat special in terms of both signature validation, // we need to investigate these files further. The PdfValidator is made specifically // for this purpose. diff --git a/src/ByteGuard.FileValidator/Models/FileDefinition.cs b/src/ByteGuard.FileValidator/Models/FileDefinition.cs index 2700c2f..df623d7 100644 --- a/src/ByteGuard.FileValidator/Models/FileDefinition.cs +++ b/src/ByteGuard.FileValidator/Models/FileDefinition.cs @@ -10,6 +10,16 @@ internal class FileDefinition /// public string FileType { get; set; } = default!; + /// + /// Whether this file type is allowed to omit a binary signature. Defaults to false. + /// + /// + /// Some file types, such as .txt files, do not have a binary signature. + /// Setting this property to true allows files of this type to be considered valid even if they do not have a binary signature. + /// This is useful for file types that do not have a binary signature, but still need to be validated by other mechanisms. + /// + public bool AllowMissingSignature { get; set; } = false; + /// /// Valid header signatures. /// diff --git a/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs b/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs index 35bd941..facb7fc 100644 --- a/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs +++ b/tests/ByteGuard.FileValidator.Tests.Unit/FileValidatorTests.cs @@ -418,6 +418,27 @@ public void HasValidSignature_FileNameDoesNotHaveExtension_ShouldThrowArgumentEx Assert.Throws(act); } + [Theory(DisplayName = "HasValidSignature(byte[]) should skip signature validation for file types that allow missing signatures and return true")] + [InlineData(new byte[] { 0x74, 0x65, 0x73, 0x74 }, "test.txt")] // TXT + [InlineData(new byte[] { 0x74, 0x65, 0x73, 0x74, 0x3B, 0x63, 0x6F, 0x6C, 0x75, 0x6D, 0x6E, 0x31 }, "test.csv")] // CSV + public void HasValidSignature_FileTypeAllowsMissingSignature_ShouldReturnTrue(byte[] fileBytes, string fileName) + { + // Arrange + var config = new FileValidatorConfiguration + { + SupportedFileTypes = [Path.GetExtension(fileName)], + FileSizeLimit = ByteSize.MegaBytes(25), + ThrowExceptionOnInvalidFile = true + }; + var fileValidator = new FileValidator(config); + + // Act + var result = fileValidator.HasValidSignature(fileName, fileBytes); + + // Assert + Assert.True(result); + } + [Theory(DisplayName = "IsOpenXmlFormat should return true for valid Open XML files")] [InlineData("test.docx")] // DOCX [InlineData("test.xlsx")] // XLSX