Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ It helps you enforce consistent file upload rules by checking:
- File size limits
- File signatures (magic numbers) to detect spoofed types
- Specification conformance for Office Open XML / Open Document Formats (`.docx`, `.xlsx`, `.pptx`, `.odt`)
- Malware scan result using a varity of scanners (_requires the addition of a specific ByteGuard.FileValidator scanner package_)

> ⚠️ **Important:** This library should be part of a **defense-in-depth** strategy.
It does not replace antivirus scanning, sandboxing, or other security controls.
Expand All @@ -17,6 +18,7 @@ It does not replace antivirus scanning, sandboxing, or other security controls.
- ✅ Validate files by **size**
- ✅ Validate files by **signature (_magic-numbers_)**
- ✅ Validate files by **specification conformance** for archive-based formats (_Open XML and Open Document Formats_)
- ✅ **Ensure no malware** through a variety of antimalware scanners
- ✅ Validate using file path, `Stream`, or `byte[]`
- ✅ Configure which file types to support
- ✅ Configure whether to **throw exceptions** or simply return a boolean
Expand All @@ -25,23 +27,26 @@ It does not replace antivirus scanning, sandboxing, or other security controls.
## Getting Started

### Installation
This package is published and installed via NuGet.
This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.FileValidator).

Reference the package in your project:
```bash
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. Youo can find the relevant scanner package on NuGet under the namespace `ByteGuard.FileValidator.Scanners`.
## Usage

### Basic validation

```csharp
var configuration = new FileValidatorConfiguration
{
SupportedFileTypes = [FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png],
FileSizeLimit = ByteSize.MegaBytes(25),
ThrowExceptionOnInvalidFile = false
SupportedFileTypes = [FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png],
FileSizeLimit = ByteSize.MegaBytes(25),
ThrowExceptionOnInvalidFile = false,
AntimalwareScanner = new SpecificAntimalwareScanner(scannerOptions)
};

var fileValidator = new FileValidator(configuration);
Expand All @@ -52,10 +57,11 @@ var isValid = fileValidator.IsValidFile("example.pdf", fileStream);

```csharp
var configuration = new FileValidatorConfigurationBuilder()
.AllowFileTypes(FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png)
.SetFileSizeLimit(ByteSize.MegaBytes(25))
.SetThrowExceptionOnInvalidFile(false)
.Build();
.AllowFileTypes(FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png)
.SetFileSizeLimit(ByteSize.MegaBytes(25))
.SetThrowExceptionOnInvalidFile(false)
.AddAntimalwareScanner(new SpecificAntimalwareScanner(scannerOptions))
.Build();

var fileValidator = new FileValidator(configuration);
var isValid = fileValidator.IsValidFile("example.pdf", fileStream);
Expand All @@ -72,13 +78,15 @@ The `FileValidator` class provides methods to validate specific aspects of a fil
> 2. File size validation
> 3. Signature (magic-number) validation
> 4. Optional Open XML / Open Document Format specification conformance validation (for supported types)
> 5. Optional antimalware scanning with a compatible scanning package

```csharp
bool isExtensionValid = fileValidator.IsValidFileType(fileName);
bool isFileSizeValid = fileValidator.HasValidSize(fileStream);
bool isSignatureValid = fileValidator.HasValidSignature(fileName, fileStream);
bool isOpenXmlValid = fileValidator.IsValidOpenXmlDocument(fileName, fileStream);
bool isOpenDocumentFormatValid = fileValidator.IsValidOpenDocumentFormat(fileName, fileStream);
bool isMalwareClean = fileValidator.IsMalwareClean(fileName, fileStream);
```

### Example
Expand All @@ -92,7 +100,8 @@ public async Task<IActionResult> Upload(IFormFile file)
{
SupportedFileTypes = [FileExtensions.Pdf, FileExtensions.Docx],
FileSizeLimit = ByteSize.MegaBytes(10),
ThrowExceptionOnInvalidFile = false
ThrowExceptionOnInvalidFile = false,
AntimalwareScanner = new SpecificAntimalwareScanner(scannerOptions)
};

var validator = new FileValidator(configuration);
Expand Down Expand Up @@ -132,9 +141,10 @@ The following file extensions are supported by the `FileValidator`:

`IsValidFile` always validates:

- File extension (against `SupportedFileTypes`)
- File size (against `FileSizeLimit`)
- File signature (magic number)
- 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:

Expand All @@ -143,11 +153,13 @@ For some formats, additional checks are performed:
- File size
- Signature
- Specification conformance
- Malware scan result

- **Other binary formats** (e.g. images, audio, video such as `.jpg`, `.png`, `.mp3`, `.mp4`):
- Extension
- File size
- Signature
- Malware scan result

## Configuration Options

Expand All @@ -158,6 +170,7 @@ The `FileValidatorConfiguration` supports:
| `SupportedFileTypes` | Yes | N/A | A list of allowed file extensions (e.g., `.pdf`, `.jpg`).<br>Use the predefined constants in `FileExtensions` for supported types. |
| `FileSizeLimit` | Yes | N/A | Maximum permitted size of files.<br>Use the static `ByteSize` class provided with this package, to simplify your limit. |
| `ThrowExceptionOnInvalidFile` | No | `true` | Whether to throw an exception on invalid files or return `false`. |
| `AntimalwareScanner` | No | N/A | An antimalware scanner used to scan the given file for potential malware. |

### Exceptions

Expand All @@ -171,6 +184,7 @@ When `ThrowExceptionOnInvalidFile` is set to `true`, validation functions will t
| `InvalidSignatureException` | Thrown when the file's signature does not match the expected signature for its type. |
| `InvalidOpenXmlFormatException` | Thrown when the internal structure of an Open XML file is invalid (`.docx`, `.xlsx`, `.pptx`, etc.). |
| `InvalidOpenDocumentFormatException` | Thrown when the specification conformance of an Open Document Format file is invalid (`.odt`, etc.). |
| `MalwareDetectedException` | Thrown when the configured antimalware scanner detected malware in the file from a scan result. |

## When to use this package

Expand Down
2 changes: 1 addition & 1 deletion src/ByteGuard.FileValidator/ByteGuard.FileValidator.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net48;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
<Authors>ByteGuard Contributors, detilium</Authors>
<Description>ByteGuard File Validator is a security-focused .NET library for validating user-supplied files, providing a configurable API to help you enforce safe and consistent file handling across your applications.</Description>
<PackageProjectUrl>https://github.com/ByteGuard-HQ/byteguard-file-validator-net</PackageProjectUrl>
Expand Down
3 changes: 1 addition & 2 deletions src/ByteGuard.FileValidator/ByteSize.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Globalization;

namespace ByteGuard.FileValidator
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Linq;
using ByteGuard.FileValidator.Exceptions;
using ByteGuard.FileValidator.Exceptions;

namespace ByteGuard.FileValidator.Configuration
{
Expand Down Expand Up @@ -49,4 +47,4 @@ public static void ThrowIfInvalid(FileValidatorConfiguration configuration)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using ByteGuard.FileValidator.Exceptions;
using ByteGuard.FileValidator.Scanners;

namespace ByteGuard.FileValidator.Configuration
{
Expand Down Expand Up @@ -27,33 +26,13 @@ public class FileValidatorConfiguration
public long FileSizeLimit { get; set; } = -1;

/// <summary>
/// Maximum file size limit in string representation (e.g. "25MB", "2 GB", etc.).
/// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to <c>true</c>.
/// </summary>
/// <remarks>
/// Defines the file size limit of files. See <see cref="ByteSize"/> for conversion help.
/// Will be ignored if <see cref="FileSizeLimit"/> is defined.
/// Spacing (<c>"25 MB"</c> vs. <c>"25MB"</c>) is irrelevant.
/// <para>Supported string representation are:
/// <ul>
/// <li><c>B</c>: Bytes</li>
/// <li><c>KB</c>: Kilobytes</li>
/// <li><c>MB</c>: Megabytes</li>
/// <li><c>GB</c>: Gigabytes</li>
/// </ul>
/// </para>
/// </remarks>
public string FriendlyFileSizeLimit { get; set; }
public bool ThrowExceptionOnInvalidFile { get; set; } = true;

/// <summary>
/// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to <c>true</c>.
/// Optional antimalware scanner to use during file validation.
/// </summary>
/// <remarks>
/// Will throw the following exceptions:
/// <ul>
/// <li><see cref="UnsupportedFileException"/> if the given file type is not supported according to the <see cref="SupportedFileTypes"/>.</li>
/// <li><see cref="InvalidSignatureException"/> if the file signature does not match the valid signatures for the given file type.</li>
/// </ul>
/// </remarks>
public bool ThrowExceptionOnInvalidFile { get; set; } = true;
public IAntimalwareScanner? AntimalwareScanner { get; set; } = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using ByteGuard.FileValidator.Scanners;

namespace ByteGuard.FileValidator.Configuration
{
Expand All @@ -10,6 +10,7 @@ public class FileValidatorConfigurationBuilder
private readonly List<string> supportedFileTypes = new List<string>();
private bool throwOnInvalidFiles = true;
private long fileSizeLimit = ByteSize.MegaBytes(25);
private IAntimalwareScanner? antimalwareScanner = null;

/// <summary>
/// Allow specific file types (extensions) to be validated.
Expand Down Expand Up @@ -43,6 +44,22 @@ public FileValidatorConfigurationBuilder SetFileSizeLimit(long inFileSizeLimit)
return this;
}

/// <summary>
/// Add an antimalware scanner.
/// </summary>
/// <param name="scanner">Antimalware scanner to use.</param>
/// <exception cref="ArgumentNullException">Thrown when the provided scanner is null.</exception>
public FileValidatorConfigurationBuilder AddAntimalwareScanner(IAntimalwareScanner scanner)
{
if (scanner == null)
{
throw new ArgumentNullException(nameof(scanner));
}

antimalwareScanner = scanner;
return this;
}

/// <summary>
/// Build configuration.
/// </summary>
Expand All @@ -53,12 +70,13 @@ public FileValidatorConfiguration Build()
{
SupportedFileTypes = supportedFileTypes,
ThrowExceptionOnInvalidFile = throwOnInvalidFiles,
FileSizeLimit = fileSizeLimit
FileSizeLimit = fileSizeLimit,
AntimalwareScanner = antimalwareScanner
};

ConfigurationValidator.ThrowIfInvalid(configuration);

return configuration;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace ByteGuard.FileValidator.Exceptions
{
/// <summary>
/// Exception type used specifically when a an antimalware scanner failed to scane the file.
/// </summary>
public class AntimalwareScannerException : Exception
{
/// <summary>
/// Default expcetion message for this expection type, if no custom message is provided.
/// </summary>
private const string defaultMessage = "An error occurred while scanning the file with the antimalware scanner.";

/// <summary>
/// Initializes a new instance of the <see cref="AntimalwareScannerException"/> class indicating that an error occured during the scan.
/// </summary>
public AntimalwareScannerException()
: this(defaultMessage)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AntimalwareScannerException"/> class indicating that an error occured during the scan.
/// </summary>
/// <param name="message">Custom exception message.</param>
public AntimalwareScannerException(string message) : base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AntimalwareScannerException"/> class indicating that an error occured during the scan.
/// </summary>
/// <param name="message">Custom exception message.</param>
/// <param name="innerException">Inner exception.</param>
public AntimalwareScannerException(string message, Exception innerException) : base(message, innerException)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AntimalwareScannerException"/> class indicating that an error occured during the scan.
/// </summary>
/// <param name="innerException">Inner exception.</param>
public AntimalwareScannerException(Exception innerException) : base(defaultMessage, innerException)
{
}
}
}
18 changes: 14 additions & 4 deletions src/ByteGuard.FileValidator/Exceptions/EmptyFileException.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
using System;

namespace ByteGuard.FileValidator.Exceptions
namespace ByteGuard.FileValidator.Exceptions
{
/// <summary>
/// Exception type used specifically when a given file does not contain any content.
/// </summary>
public class EmptyFileException : Exception
{
/// <summary>
/// Construct a new <see cref="EmptyFileException"/> to indicate that the provided file does not have any content.
/// </summary>
public EmptyFileException()
: this("The provided file does not have any content.")
{
}

/// <summary>
/// Construct a new <see cref="EmptyFileException"/> to indicate that the provided file does not have any content.
/// </summary>
/// <param name="message">Custom exception message.</param>
public EmptyFileException(string message) : base(message)
{
}

/// <summary>
/// Construct a new <see cref="EmptyFileException"/> to indicate that the provided file does not have any content.
/// </summary>
/// <param name="message">Custom exception message.</param>
/// <param name="innerException">Inner exception.</param>
public EmptyFileException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
using System;
using System.Runtime.Serialization;

namespace ByteGuard.FileValidator.Exceptions
namespace ByteGuard.FileValidator.Exceptions
{
/// <summary>
/// Exception type used specifically when a given file exceeds the configured file size limit.
/// </summary>
public class InvalidFileSizeException : Exception
{
/// <summary>
/// Construct a new <see cref="InvalidFileSizeException"/> to indicate that the provided file does not adhere to the configured limit.
/// </summary>
public InvalidFileSizeException()
: base("File size is larger than the configured file size limit.")
{
}

/// <summary>
/// Construct a new <see cref="InvalidFileSizeException"/> to indicate that the provided file does not adhere to the configured limit.
/// </summary>
/// <param name="message">Custom exception message.</param>
public InvalidFileSizeException(string message) : base(message)
{
}
}
}
}
Loading