# Preventing Path Traversal Attacks with File Uploads in ASP.NET Core Minimal APIs

> Learn to prevent path traversal attacks in ASP.NET Core Minimal APIs. Secure file uploads by validating filenames, content types, and enforcing path boundaries.

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

---

**Never use raw `IFormFile.FileName` when constructing server-side paths; instead generate random filenames, validate content types, and enforce strict path boundaries to block directory traversal sequences like `../`.**

File uploads in ASP.NET Core minimal APIs streamline development but introduce severe security risks when user-supplied filenames are used to construct file-system paths. The **dotnet/skills** repository provides production-ready guidance demonstrating how attackers exploit directory traversal sequences to write files outside intended directories, and how to implement robust defenses. By following the patterns established in the minimal-api-file-upload skill, you can eliminate path traversal vulnerabilities while maintaining clean, endpoint-based code.

## Why Path Traversal Is a Critical Risk in Minimal APIs

Minimal APIs remove the ceremony of traditional MVC controllers, but this brevity often leads developers to overlook input validation. When handling `IFormFile` uploads, using the raw `FileName` property directly in `Path.Combine` allows attackers to embed sequences like `../../../etc/passwd`, causing the application to write sensitive system files. The **dotnet/skills** repository explicitly warns against this anti-pattern 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) at line 158, emphasizing that the user-provided filename should never touch the file system.

## Core Defense Strategies

### Generate Random Filenames Instead of Trusting User Input

The primary defense against path traversal is eliminating attacker-controlled path components entirely. According to the guidance in [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) (line 158), you should generate a new random filename—such as a GUID—and only derive the extension after validating the content type.

```csharp
// Derive safe extension from detection, then generate a random name
var extension = detected == "image/jpeg" ? ".jpg" : ".png";
var safeName = $"{Guid.NewGuid()}{extension}";
var uploadPath = Path.Combine("uploads", safeName);

```

This approach ensures that even if an attacker uploads a file named `../../../malicious.exe`, the dangerous path segments never reach the operating system.

### Validate Content Types and Magic Bytes

Checking file extensions alone is insufficient; attackers can disguise payloads by changing extensions. The skill file at lines 121-152 mandates validating both MIME types and magic bytes (file signatures) to verify the file's true format.

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

// Magic-byte check
await using var stream = file.OpenReadStream();
var header = new byte[8];
var read = await stream.ReadAsync(header, 0, header.Length);
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;

```

Verify that the detected content matches the declared MIME type before writing any bytes to disk.

### Sanitize Path Components with Root Directory Checks

When you must incorporate user-provided directory names (not filenames), implement defensive path resolution. The [`LocalSessionFsHandler.cs`](https://github.com/dotnet/skills/blob/main/LocalSessionFsHandler.cs) file at line 35 demonstrates a security check that resolves the full path and verifies it remains within a trusted root directory.

```csharp
string SafeCombine(string baseDir, string userPath)
{
    var fullPath = Path.GetFullPath(Path.Combine(baseDir, userPath));
    if (!fullPath.StartsWith(Path.GetFullPath(baseDir), StringComparison.Ordinal))
        throw new UnauthorizedAccessException($"Path traversal blocked: {userPath}");
    return fullPath;
}

```

This pattern throws `UnauthorizedAccessException` immediately if the resolved path escapes the intended upload folder.

### Enforce Request Size Limits

Prevent denial-of-service attacks by configuring limits at both the Kestrel server and form parser levels. The [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) at lines 55-68 recommends setting `MaxRequestBodySize` and `MultipartBodyLengthLimit` to appropriate values, such as 10 MB for image uploads.

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 10 * 1024 * 1024);
builder.Services.Configure<FormOptions>(o => o.MultipartBodyLengthLimit = 10 * 1024 * 1024);

```

### Handle Anti-Forgery Tokens Correctly

By default, ASP.NET Core validates antiforgery tokens on form-bound endpoints. For API-only uploads using JWT authentication, you must explicitly disable this check to avoid 400 errors. The skill file at lines 96-106 shows the correct pattern:

```csharp
app.MapPost("/api/upload", (IFormFile file) => Results.Ok(file.FileName))
   .DisableAntiforgery();   // safe when using JWT or no auth

```

Only disable antiforgery when you have alternative authentication mechanisms in place.

## Complete Implementation Example

Combine these safeguards into a single minimal API endpoint that handles uploads securely:

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

    // 2. Validate magic bytes
    await using var stream = file.OpenReadStream();
    var header = new byte[8];
    var read = await stream.ReadAsync(header, 0, header.Length);
    if (read < 4) return Results.BadRequest("File 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;
    var detected = isJpeg ? "image/jpeg" : isPng ? "image/png" : null;
    if (detected is null) return Results.BadRequest("Invalid image format");
    if (!string.Equals(detected, file.ContentType, StringComparison.OrdinalIgnoreCase))
        return Results.BadRequest("Content-Type mismatch");

    // 3. Generate safe filename
    var extension = detected == "image/jpeg" ? ".jpg" : ".png";
    var safeName = $"{Guid.NewGuid()}{extension}";
    var uploadPath = Path.Combine("uploads", safeName);
    Directory.CreateDirectory("uploads");

    // 4. Optional: Verify path is within upload directory
    if (!Path.GetFullPath(uploadPath).StartsWith(Path.GetFullPath("uploads"), StringComparison.Ordinal))
        return Results.BadRequest("Invalid path");

    // 5. Write file
    stream.Position = 0;
    await using var outFile = File.Create(uploadPath);
    await stream.CopyToAsync(outFile);

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

```

This implementation follows the exact patterns recommended in the dotnet/skills repository, preventing path traversal through randomized names, content validation, and path verification.

## Summary

- **Never trust `IFormFile.FileName`** for file system operations; generate GUID-based filenames and derive extensions only after validation.
- **Validate both MIME types and magic bytes** to ensure files match their claimed formats, as detailed in [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) lines 121-152.
- **Implement root-directory checks** using `Path.GetFullPath` comparisons to block traversal attempts, mirroring the logic in [`LocalSessionFsHandler.cs`](https://github.com/dotnet/skills/blob/main/LocalSessionFsHandler.cs) line 35.
- **Configure size limits** in both Kestrel (`MaxRequestBodySize`) and `FormOptions` (`MultipartBodyLengthLimit`) to prevent DoS attacks.
- **Manage antiforgery tokens** appropriately—disable only for API endpoints with alternative authentication, as shown in [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) lines 96-106.

## Frequently Asked Questions

### Can I use the original filename if I sanitize it?

While sanitization is possible, the dotnet/skills repository strongly recommends against it. According to the guidance at line 158 in [`minimal-api-file-upload/SKILL.md`](https://github.com/dotnet/skills/blob/main/minimal-api-file-upload/SKILL.md), generating a completely new random filename (like a GUID) eliminates all path traversal risks. If you must preserve the original name, apply rigorous whitelist-based sanitization and still verify the final path resolves within your upload directory.

### How do I validate file content beyond extensions?

Inspect the file's magic bytes—the specific byte sequences that identify file types. For example, JPEG files start with `0xFF 0xD8 0xFF` and PNG files with `0x89 0x50 0x4E 0x47`. Read the first 8 bytes of the stream and compare them against known signatures, then verify they match the declared `ContentType`. This prevents attackers from uploading executable code disguised as images.

### What size limits should I set for file uploads?

Set limits based on your specific use case and available memory. The examples in [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) lines 55-68 demonstrate 10 MB limits using `MaxRequestBodySize` for Kestrel and `MultipartBodyLengthLimit` for form parsing. For production applications, consider smaller limits (1-5 MB) for images and implement chunked uploading or streaming for larger files to avoid memory exhaustion.

### When should I disable antiforgery in minimal APIs?

Disable antiforgery only for API endpoints that do not use cookie-based authentication. As shown in [`SKILL.md`](https://github.com/dotnet/skills/blob/main/SKILL.md) lines 96-106, calling `.DisableAntiforgery()` is appropriate when your minimal API uses JWT tokens, API keys, or other stateless authentication. For form-based uploads in browser applications, keep antiforgery enabled to prevent cross-site request forgery attacks.