# Validating Content-Type Headers with Magic Bytes for File Upload Security in .NET Minimal APIs

> Secure file uploads in .NET Minimal APIs by validating Content-Type headers against magic bytes. Prevent MIME-type spoofing and enhance security with this essential technique.

- Repository: [.NET Platform/skills](https://github.com/dotnet/skills)
- Tags: how-to-guide
- Published: 2026-07-11

---

**Validate uploaded files by inspecting their magic bytes (file signatures) to ensure the actual content matches the declared Content-Type header, preventing MIME-type spoofing attacks in ASP.NET Core Minimal APIs.**

The `dotnet/skills` repository provides a production-ready pattern for secure file uploads that combines header validation with byte-level inspection. Located in the `dotnet-aspnetcore` plugin under [`plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md), this implementation demonstrates how to validate Content-Type headers against magic bytes to mitigate spoofing risks in .NET 8+ Minimal APIs.

## The Risk of Trusting Content-Type Headers Alone

Attackers can easily spoof MIME types by setting a malicious file's Content-Type header to `image/jpeg` while the actual payload contains executable code. Relying solely on the `IFormFile.ContentType` property exposes your application to malware uploads and remote code execution vulnerabilities. The `dotnet/skills` implementation addresses this by requiring cryptographic verification of the file's actual structure through magic byte inspection.

## Defense-in-Depth Validation Layers

The skill implements a layered security architecture as defined in [`plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md). Each layer addresses a specific attack vector:

### Enforcing Request Size Limits

Prevent denial-of-service attacks by configuring both Kestrel and FormOptions limits before processing uploads.

```csharp
builder.WebHost.ConfigureKestrel(opts =>
{
    opts.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MiB
});

builder.Services.Configure<FormOptions>(opts =>
{
    opts.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10 MiB
    opts.ValueLengthLimit = 1 * 1024 * 1024;        // 1 MiB for form fields
});

```

### Validating Declared Content Types

Check the `file.ContentType` property against an explicit allowlist before attempting to process the stream.

```csharp
var allowedTypes = new[] { "image/jpeg", "image/png" };
if (!allowedTypes.Contains(file.ContentType, StringComparer.OrdinalIgnoreCase))
    return Results.BadRequest("File type not allowed");

```

### Verifying Magic Bytes (File Signatures)

Read the first 8 bytes of the stream to detect the actual file format. JPEG files start with the hex sequence `FF D8 FF`, while PNG files begin with `89 50 4E 47`.

```csharp
using var stream = file.OpenReadStream();
var header = new byte[8];
var bytesRead = await stream.ReadAsync(header, 0, header.Length);
if (bytesRead < 4) return Results.BadRequest("File content is too short");

bool isJpeg = header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF;
bool isPng  = header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47;
string? detected = isJpeg ? "image/jpeg" : isPng ? "image/png" : null;

if (detected is null) return Results.BadRequest("Unsupported file format");

// Ensure declared and detected types match
if (!string.Equals(file.ContentType, detected, StringComparison.OrdinalIgnoreCase))
    return Results.BadRequest("Declared Content-Type does not match file content");

```

### Generating Safe Filenames

Eliminate path-traversal vulnerabilities by using `Guid.NewGuid()` for filenames and deriving extensions from the verified content type rather than the original upload name.

```csharp
var extension = detected == "image/jpeg" ? ".jpg" : ".png";
var safeFileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine("uploads", safeFileName);

```

## Complete Implementation Example

The following endpoint from [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) demonstrates the complete validation pipeline, combining allowlist checking, magic byte verification, and safe storage:

```csharp
app.MapPost("/upload", async (IFormFile file) =>
{
    // Allowed MIME types
    var allowedTypes = new[] { "image/jpeg", "image/png" };
    if (!allowedTypes.Contains(file.ContentType, StringComparer.OrdinalIgnoreCase))
        return Results.BadRequest("File type not allowed");

    // Read first bytes to detect real format
    using var stream = file.OpenReadStream();
    var header = new byte[8];
    var bytesRead = await stream.ReadAsync(header, 0, header.Length);
    if (bytesRead < 4) return Results.BadRequest("File content is too short");

    bool isJpeg = header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF;
    bool isPng  = header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47;
    string? detected = isJpeg ? "image/jpeg" : isPng ? "image/png" : null;
    if (detected is null) return Results.BadRequest("Unsupported file format");

    // Ensure declared and detected types match
    if (!string.Equals(file.ContentType, detected, StringComparison.OrdinalIgnoreCase))
        return Results.BadRequest("Declared Content-Type does not match file content");

    // Generate a safe filename
    var extension = detected == "image/jpeg" ? ".jpg" : ".png";
    var safeFileName = $"{Guid.NewGuid()}{extension}";
    var filePath = Path.Combine("uploads", safeFileName);
    Directory.CreateDirectory("uploads");
    stream.Position = 0;
    using var outStream = File.Create(filePath);
    await stream.CopyToAsync(outStream);

    return Results.Ok(new { FileName = safeFileName, file.Length });
});

```

## Streaming Large Files Without Buffering

For memory-efficient processing of large uploads, the skill demonstrates a low-level `MultipartReader` implementation that parses the multipart body and streams each section directly to disk without buffering the entire payload in memory. This approach is defined in [`plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md) and is essential for high-traffic scenarios.

```csharp
app.MapPost("/upload-stream",
    [DisableRequestSizeLimit] async (HttpContext ctx) =>
    {
        var contentType = ctx.Request.ContentType;
        if (contentType == null) return Results.BadRequest("Missing Content-Type");

        if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaType))
            return Results.BadRequest("Invalid Content-Type");

        var boundary = HeaderUtilities.RemoveQuotes(mediaType.Boundary).Value;
        if (string.IsNullOrWhiteSpace(boundary)) return Results.BadRequest("Not a multipart request");

        var reader = new MultipartReader(boundary, ctx.Request.Body);
        while (await reader.ReadNextSectionAsync() is { } section)
        {
            if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var cd))
                continue;

            if (cd.DispositionType.Equals("form-data") &&
                !string.IsNullOrEmpty(cd.FileName.Value))
            {
                var safeFile = $"{Guid.NewGuid()}";
                Directory.CreateDirectory("uploads");
                using var fs = File.Create(Path.Combine("uploads", safeFile));
                await section.Body.CopyToAsync(fs);
            }
        }

        return Results.Ok("Uploaded");
    }).DisableAntiforgery();

```

## Configuration Best Practices

The `dotnet/skills` repository includes validation scenarios in [`plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/eval.yaml`](https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/eval.yaml) that verify the solution checks actual file content rather than just headers. The CI pipeline uses [`eng/skill-validator/src/Program.cs`](https://github.com/dotnet/skills/blob/main/eng/skill-validator/src/Program.cs) to execute these validation tests automatically.

For production deployments, enable anti-forgery protection on form-based uploads:

```csharp
builder.Services.AddAntiforgery();
app.UseAntiforgery();

```

API-only endpoints can opt-out using `.DisableAntiforgery()` when implementing token-based authentication instead.

## Summary

- **Validate Content-Type headers** against an explicit allowlist before processing uploads
- **Verify magic bytes** by reading the first 8 bytes of the stream to confirm the actual file format matches the declared MIME type
- **Generate safe filenames** using `Guid.NewGuid()` with extensions derived from verified content types, never from the original filename
- **Configure size limits** at both the Kestrel (`MaxRequestBodySize`) and FormOptions (`MultipartBodyLengthLimit`) layers to prevent DoS attacks
- **Stream large files** using `MultipartReader` to avoid memory exhaustion on high-traffic endpoints

## Frequently Asked Questions

### What are magic bytes and why are they important for file upload security?

Magic bytes are hexadecimal signatures at the beginning of files that identify the actual format regardless of the file extension or Content-Type header. They are crucial because HTTP headers can be easily spoofed by attackers attempting to upload executable files disguised as images. By checking for JPEG (`FF D8 FF`) or PNG (`89 50 4E 47`) signatures, you verify the file's true nature before processing.

### How does the dotnet/skills implementation prevent path traversal attacks?

The implementation eliminates path traversal vulnerabilities by completely disregarding the original filename provided by the client. Instead, it generates safe filenames using `Guid.NewGuid()` and appends extensions (`.jpg` or `.png`) only after verifying the content type through magic byte inspection. This ensures malicious filenames like `../../../etc/passwd` cannot influence the storage location.

### What is the difference between IFormFile and MultipartReader approaches?

`IFormFile` buffers the entire upload in memory or on disk by default, making it suitable for small files but risky for large uploads or high-traffic scenarios. `MultipartReader` processes the HTTP request stream directly without buffering, allowing you to handle gigabyte-scale uploads with minimal memory footprint. The `dotnet/skills` repository provides examples of both approaches in [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) to accommodate different scalability requirements.

### Can this validation be bypassed by polyglot files?

While magic byte validation catches simple MIME-type spoofing, sophisticated polyglot files (files valid in multiple formats) may require additional validation. The `dotnet/skills` pattern mitigates this by combining byte verification with size limits and safe filename generation. For highly sensitive applications, consider adding dimension verification (for images) or sandboxed processing as defense-in-depth layers beyond the magic byte checks.