# How to Validate File Content Using Magic Bytes in ASP.NET Core File Uploads

> Secure ASP.NET Core file uploads by validating content with magic bytes. Prevent spoofed headers and malicious files by inspecting file streams.

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

---

**Validate file uploads by inspecting the first few bytes of the stream to verify the actual file type matches the declared Content-Type, preventing spoofed headers and malicious file disguises.**

Relying solely on the `Content-Type` header or filename extension is insecure because both can be forged by malicious clients. The `dotnet/skills` repository demonstrates a robust defense pattern in its minimal API file upload skill, which inspects **magic bytes** (file signatures) to confirm the true format before processing. This approach protects your application from content-type spoofing and path traversal attacks while ensuring only legitimate files reach your storage layer.

## Why Magic Bytes Matter for File Upload Security

Magic bytes are the hexadecimal signatures embedded in the header of every standardized file format. Unlike metadata that travels separately in HTTP headers, these bytes represent the actual content structure of the file.

| Threat Vector | Header/Extension Validation | Magic Byte Defense |
|--------------|---------------------------|-------------------|
| **Content-Type spoofing** | Client can set arbitrary MIME types | Reads actual binary signature from file content |
| **Extension masquerading** | Renaming `malware.exe` to `photo.jpg` bypasses checks | Signature reveals true format regardless of filename |
| **Path traversal** | Using `file.FileName` directly risks `../` sequences | Server-generated safe names eliminate injection vulnerabilities |

According to the source code in `dotnet/skills`, the validation logic resides 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) (lines 21–53), which demonstrates how to read these signatures before persisting any uploaded content.

## Implementing Magic Byte Validation in Minimal APIs

The validation flow follows four critical steps to **validate file content using magic bytes in ASP.NET Core file uploads** securely.

### Reading the File Header

First, open the upload stream and capture the signature bytes. Reading only the initial bytes is memory-efficient and does not require loading the entire file into memory.

```csharp
await using var stream = file.OpenReadStream();
var header = new byte[8];
int bytesRead = await stream.ReadAsync(header, 0, header.Length);

if (bytesRead < 4)
    return Results.BadRequest("File content is too short or invalid");

```

### Detecting Known Signatures

Compare the extracted bytes against documented magic numbers. As implemented in `dotnet/skills`, common image signatures include:

- **JPEG**: `0xFF 0xD8 0xFF` (starts at offset 0)
- **PNG**: `0x89 0x50 0x4E 0x47` (ASCII `‰PNG`)

```csharp
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;

```

### Validating Against Declared Content-Type

Derive the actual MIME type from the detected signature and enforce strict matching with the client-declared `Content-Type`. This step catches spoofed headers that claim benign types while carrying malicious payloads.

```csharp
string? detectedContentType = isJpeg ? "image/jpeg" 
                                     : isPng ? "image/png" 
                                     : null;

if (!string.Equals(file.ContentType, detectedContentType, StringComparison.OrdinalIgnoreCase))
    return Results.BadRequest("Content-Type header does not match actual file content.");

```

### Generating Safe Filenames

Never trust `file.FileName` for storage paths, as it may contain path-traversal sequences like `../../etc/passwd`. Instead, generate a server-side safe name using the detected extension.

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

```

## Complete Production-Ready Example

Below is the consolidated endpoint implementation extracted from the `dotnet/skills` repository, showing the full validation pipeline from size checks to secure persistence:

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

    // 2. Read magic bytes
    await using var stream = file.OpenReadStream();
    var header = new byte[8];
    int bytesRead = await stream.ReadAsync(header, 0, header.Length);
    if (bytesRead < 4)
        return Results.BadRequest("File content is too short or invalid");

    // 3. Detect JPEG / PNG signatures
    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? detectedContentType = isJpeg ? "image/jpeg"
                                         : isPng ? "image/png"
                                         : null;
    if (detectedContentType is null)
        return Results.BadRequest("Unsupported image format.");

    // 4. Ensure declared Content-Type matches signature
    if (!string.Equals(file.ContentType, detectedContentType, StringComparison.OrdinalIgnoreCase))
        return Results.BadRequest("Content-Type header does not match actual file content.");

    // 5. Generate safe filename
    string extension = detectedContentType == "image/jpeg" ? ".jpg" : ".png";
    string safeFileName = $"{Guid.NewGuid()}{extension}";
    string filePath = Path.Combine("uploads", safeFileName);
    Directory.CreateDirectory("uploads");

    // 6. Persist the file
    stream.Position = 0;  // rewind after header read
    await using var fileStream = File.Create(filePath);
    await stream.CopyToAsync(fileStream);

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

```

## Testing and Validation

The `dotnet/skills` repository includes automated test suites that verify the magic-byte validation logic:

- [`tests/dotnet-aspnetcore/minimal-api-file-upload/eval.yaml`](https://github.com/dotnet/skills/blob/main/tests/dotnet-aspnetcore/minimal-api-file-upload/eval.yaml) — Standard evaluation tests ensuring the endpoint rejects files with mismatched signatures.
- [`tests/dotnet-aspnetcore/minimal-api-file-upload/eval.vally.yaml`](https://github.com/dotnet/skills/blob/main/tests/dotnet-aspnetcore/minimal-api-file-upload/eval.vally.yaml) — Vally-compatible test definitions used in the skill-validator pipeline to prevent regression.

These files ensure that the magic-byte checks remain functional as the codebase evolves, providing both reference implementation and continuous validation.

## Summary

- **Validate file content using magic bytes in ASP.NET Core file uploads** by reading the first 4–8 bytes of the stream and comparing them against known signatures like `FF D8 FF` for JPEG or `89 50 4E 47` for PNG.
- Always cross-reference the detected signature against the declared `Content-Type` to catch spoofed headers.
- Generate server-side safe filenames using `Guid.NewGuid()` rather than trusting `file.FileName` to prevent path traversal attacks.
- Reference the complete implementation 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) within the `dotnet/skills` repository.

## Frequently Asked Questions

### What are magic bytes in file validation?

Magic bytes are hexadecimal signatures located at the beginning of a file's binary structure that identify its true format. Unlike file extensions or MIME headers, which are easily modified, magic bytes are embedded in the content itself—such as `FF D8` for JPEG images—making them reliable for verifying actual file types during upload processing.

### Why is checking the file extension not secure?

File extensions are arbitrary metadata strings that can be renamed instantly by users or malicious scripts. A file named `report.pdf` might actually contain executable code with a forged extension. Magic byte validation reads the actual binary structure, ensuring the content genuinely matches the expected format regardless of what the filename claims.

### How many bytes should I read for reliable validation?

Most common file formats can be identified within the first 4 to 8 bytes. For JPEG validation, reading 3 bytes is sufficient (`FF D8 FF`), while PNG requires 4 bytes (`89 50 4E 47`). The `dotnet/skills` implementation reads 8 bytes to accommodate future format detection while remaining memory-efficient for large uploads.

### Where can I find the reference implementation for this pattern?

The authoritative implementation resides in the `dotnet/skills` repository at [`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) (lines 21–53). This file contains the complete minimal API endpoint with magic-byte validation, safe filename generation, and persistence logic used by the skill's automated testing pipeline.