File uploads are a common feature in web applications. Users upload profile pictures, documents, invoices, reports, and other files. In ASP.NET Core, handling an uploaded file is straightforward, but accepting files without proper validation can create serious security problems.
One common mistake is to trust the file extension. For example, an application may allow files ending in .jpg and reject everything else. However, changing a file's extension does not change its actual content. A malicious file can be renamed to photo.jpg and still contain something completely different.
A better approach is to check the file signature, also known as its magic bytes. These are specific byte sequences found at the beginning of many file formats.
In this article, we will learn how to validate file signatures in ASP.NET Core, combine signature checks with other security measures, and build a safer file upload endpoint.
Why File Extension Validation Is Not Enough
Most applications use the file extension to decide whether an upload is allowed. This is useful as an initial check, but it cannot prove that a file contains valid image or document data.
Consider the following example:
string fileName = "invoice.jpg";
if (Path.GetExtension(fileName).Equals(
".jpg",
StringComparison.OrdinalIgnoreCase))
{
// Accept the file
}This code checks only the filename. It does not read the file's contents.
An attacker could rename an executable file, script, or other unwanted content to invoice.jpg. The extension check would still pass.
There are three common ways to inspect an uploaded file.
Validation method | What it checks | Limitation |
|---|---|---|
File extension | The filename suffix | Can be changed easily |
MIME type | The declared content type | Can be supplied or changed by the client |
File signature | Known bytes inside the file | Does not prove the entire file is valid or safe |
File signature validation provides stronger evidence about the file format. However, it should be combined with extension checks, size limits, safe storage, and additional validation where necessary.
What Is a File Signature?
A file signature is a sequence of bytes that identifies a file format. Many common formats begin with recognizable byte patterns.
For example:
File format | Typical signature in hexadecimal |
|---|---|
JPEG | FF D8 FF |
PNG | 89 50 4E 47 0D 0A 1A 0A |
25 50 44 46 2D | |
GIF | 47 49 46 38 |
These bytes appear at the beginning of typical files in the corresponding formats.
When a user uploads a file, the application reads a small portion of its contents and compares those bytes with the expected signature.
If a file has a .png extension but its contents begin with an unrelated signature, the application should reject it.
One important limitation is that signatures can be copied or forged. A file can start with the correct bytes and still contain malformed or malicious content. Signature checking is one layer of validation, not a complete security solution.
Step 1: Define the Allowed File Types
Start by deciding which file formats the application actually needs. Avoid accepting every file type when the application only requires images.
For this example, we will allow JPEG, PNG, and PDF files.
Create a validation service that checks the extension and the file signature.
using Microsoft.AspNetCore.Http;
public static class FileSignatureValidator
{
private static readonly Dictionary<string, byte[]> Signatures =
new(StringComparer.OrdinalIgnoreCase)
{
[".jpg"] = new byte[] { 0xFF, 0xD8, 0xFF },
[".jpeg"] = new byte[] { 0xFF, 0xD8, 0xFF },
[".png"] = new byte[]
{
0x89, 0x50, 0x4E, 0x47,
0x0D, 0x0A, 0x1A, 0x0A
},
[".pdf"] = new byte[]
{
0x25, 0x50, 0x44, 0x46, 0x2D
}
};
public static async Task<bool> IsValidAsync(
IFormFile file,
CancellationToken cancellationToken = default)
{
if (file.Length == 0)
return false;
string extension =
Path.GetExtension(file.FileName);
if (string.IsNullOrWhiteSpace(extension) ||
!Signatures.TryGetValue(extension, out byte[]? signature))
{
return false;
}
if (file.Length < signature.Length)
return false;
byte[] header = new byte[signature.Length];
await using var stream = file.OpenReadStream();
int totalRead = 0;
while (totalRead < header.Length)
{
int bytesRead = await stream.ReadAsync(
header.AsMemory(totalRead),
cancellationToken);
if (bytesRead == 0)
return false;
totalRead += bytesRead;
}
return header.SequenceEqual(signature);
}
}How the validation code works
The Signatures dictionary stores the allowed extensions and their expected byte sequences. Using a dictionary makes it easier to add or remove supported formats without spreading validation logic across the application.
The Path.GetExtension() method extracts the extension from the original filename. The extension is used only to select the expected signature. It is not treated as proof that the file is valid.
The method then reads only the required number of bytes from the beginning of the uploaded file. It does not load the entire file into memory for signature checking.
Finally, SequenceEqual() compares the bytes read from the upload with the expected signature.
The method returns false if the file is empty, the extension is not allowed, the file is too short, or the signature does not match.
This example intentionally checks only the file header. Production applications may need format-specific parsing or additional validation to confirm that the complete file is structurally valid.
Step 2: Create a Secure File Upload Endpoint
Now that the validator is ready, we can use it in an ASP.NET Core controller.
The endpoint should check the upload size, validate the signature, generate a server-controlled filename, and store the file in a location that is not directly accessible through the web server.
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/files")]
public class FilesController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
private const long MaxFileSize = 5 * 1024 * 1024;
public FilesController(IWebHostEnvironment environment)
{
_environment = environment;
}
[HttpPost("upload")]
[RequestSizeLimit(MaxFileSize + 64 * 1024)]
public async Task<IActionResult> Upload(
IFormFile? file,
CancellationToken cancellationToken)
{
if (file is null || file.Length == 0)
{
return BadRequest("Please select a file.");
}
if (file.Length > MaxFileSize)
{
return BadRequest("The file exceeds the 5 MB limit.");
}
bool isValid = await FileSignatureValidator.IsValidAsync(
file, cancellationToken);
if (!isValid)
{
return BadRequest(
"The file type is not supported or its signature is invalid.");
}
string uploadDirectory = Path.Combine(
_environment.ContentRootPath,
"App_Data",
"Uploads");
Directory.CreateDirectory(uploadDirectory);
string extension = Path.GetExtension(file.FileName)
.ToLowerInvariant();
string storedFileName =
$"{Guid.NewGuid():N}{extension}";
string destinationPath = Path.Combine(
uploadDirectory,
storedFileName);
await using var outputStream = new FileStream(
destinationPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None);
await file.CopyToAsync(
outputStream,
cancellationToken);
return Ok(new
{
Message = "File uploaded successfully.",
FileId = storedFileName
});
}
}
Join the conversation! Your thoughts help the community grow.