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:

C#
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

PDF

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.

C#
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.

C#
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
        });
    }
}

Important parts of the implementation

File size validation: The endpoint rejects files larger than 5 MB before performing signature validation or saving them. This application-level check does not replace request-size limits at the server and hosting layers.

Server-generated filename: The application creates a new filename using Guid.NewGuid(). This avoids trusting the original filename and reduces the risk of filename collisions.

Storage location: The file is stored under App_Data/Uploads, relative to the application's content root. This directory should not be configured for direct public access. In a production environment, private object storage or a dedicated file storage service may be a better choice.

FileMode.CreateNew: This mode creates a new file and fails if the destination already exists. It helps prevent accidental overwriting of an existing file.

CancellationToken: The token is passed to the asynchronous read and copy operations so that supported work can stop when the request is cancelled.

The example demonstrates the core validation flow. A production application should also implement authorization, upload quotas, rate limiting, storage failure handling, and appropriate logging.

Step 3: Configure Request Size Limits

Checking the file size inside the controller is important, but the request must also pass through ASP.NET Core's request-processing pipeline before the controller can inspect the uploaded file.

For example, multipart form data contains more than the file itself. It also includes boundaries and other form fields.

The [RequestSizeLimit] attribute in the previous example allows some additional space beyond the 5 MB file limit. The exact allowance should match the application's request format and hosting configuration.

You should also review the following settings:

  • Configure the maximum request body size at the web server or reverse proxy.

  • Configure multipart form limits where necessary.

  • Set a maximum number of uploads per request.

  • Apply per-user or per-account storage quotas.

  • Reject oversized requests before expensive processing whenever possible.

Do not assume that a controller-level size check protects the application from every oversized request. Hosting limits and application limits should work together.

Step 4: Add More Security Checks for Production

A matching signature does not guarantee that a file is safe to open, display, or process.

Use the following controls based on the types of files your application accepts.

Validate the file with a format-aware parser

For images, consider decoding the file with a maintained image-processing library and rejecting files that cannot be decoded correctly. If the application only needs images, re-encoding them into an approved format can remove some unwanted content and metadata.

For PDFs and office documents, use appropriate parsing and security tools where the risk requires them.

Store files outside the public web root

Files should not become publicly accessible simply because an upload succeeded. Store them in private storage and provide access through an authorized download endpoint or a controlled, temporary access mechanism.

Scan files when required

Applications that accept documents from untrusted users may need antivirus scanning or a content-disarm process. If scanning is required, keep the file in quarantine until it passes the required checks.

Protect file downloads

When serving a file, authorize the requesting user and use a server-controlled content type. Consider the Content-Disposition: attachment header for files that should be downloaded rather than displayed in the browser.

Never construct a filesystem path directly from a user-supplied filename without strict validation and safe path handling.

Common Mistakes to Avoid

Mistake

Why it is a problem

Better approach

Checking only the extension

The extension can be changed

Validate the signature and format

Trusting the client MIME type

The client controls the supplied value

Determine the expected type from server-side rules

Trusting the signature alone

A matching header does not prove the whole file is safe

Parse, scan, or re-encode when appropriate

Saving with the original filename

Can create collisions or path-handling risks

Generate a server-side filename

Saving uploads in a public directory

Files may become accessible without authorization

Use private storage

Accepting unlimited uploads

Can exhaust disk space and processing capacity

Enforce size, rate, and storage limits

Troubleshooting File Signature Validation

Valid files are being rejected

Check whether the file format has multiple accepted extensions or signatures. Some formats support several valid variants, and a single fixed signature may not cover every legitimate file.

For example, a signature validator should account for the file formats the application actually intends to support rather than assuming that every file begins with one universal pattern.

Large uploads fail before reaching the controller

Review the request-size limits configured in ASP.NET Core, the hosting server, and any reverse proxy. The smallest applicable limit may reject the request before controller validation runs.

Files pass validation but fail when opened

A matching signature only confirms that the beginning of the file resembles the expected format. The remaining contents may be incomplete, corrupted, or malformed. Add format-aware validation before the file is used.

Uploads fail with access-denied errors

Check the application's filesystem permissions and the configured storage path. The application identity must have permission to create files in the destination directory. Avoid solving permission problems by granting broad write access to the server.

Advantages and Disadvantages of File Signature Validation

Advantages

  • Detects many files whose extensions do not match their contents.

  • Reads only a small portion of the file during the signature check.

  • Makes file-type validation independent of the client-supplied MIME type.

  • Allows developers to maintain a clear list of supported formats.

  • Works well as one layer in a broader file-upload security strategy.

Disadvantages

  • Does not guarantee that the complete file is valid.

  • Requires format-specific rules and maintenance.

  • Cannot reliably detect every malicious file.

  • Some formats have multiple valid signatures or more complex structures.

  • Requires additional controls for storage, scanning, authorization, and download handling.

Best Practices for Secure File Uploads

Follow these practices when building file upload features in ASP.NET Core:

  1. Allow only required formats. Maintain an explicit allowlist instead of accepting arbitrary file types.

  2. Check the extension and signature together. Neither check should be treated as complete validation.

  3. Set size limits at multiple layers. Protect the application, web server, and storage system from oversized uploads.

  4. Generate filenames on the server. Keep the original filename only as metadata if the business requires it, and handle it as untrusted input.

  5. Use private storage. Do not expose uploaded files directly through a public directory.

  6. Validate files before processing them. Use format-aware parsers, image re-encoding, or malware scanning where appropriate.

  7. Apply authorization and quotas. Restrict who can upload, access, and delete files.

  8. Handle failures safely. Clean up incomplete uploads, record useful diagnostic information, and avoid exposing internal paths or exception details to clients.

  9. Test malicious and malformed inputs. Include renamed files, truncated files, unsupported formats, oversized requests, and files with valid signatures but invalid contents.

Summary

Securing file uploads in ASP.NET Core requires more than checking a filename or MIME type. File signature validation provides a useful way to compare the uploaded content with the expected file format and reject many incorrectly named files.

A reliable implementation combines signature checks with extension allowlists, request-size limits, server-generated filenames, private storage, authorization, and appropriate content scanning or parsing.

The most important point is that a matching file signature does not make a file automatically safe. Treat every upload as untrusted input, validate it before use, and control how it is stored and accessed. These measures help build file upload features that are safer and easier to maintain.