An ASP.NET Core application that accepts file uploads gives users a way to send data directly to your server. That data might be a profile picture, a PDF document, or a spreadsheet. The problem is that an attacker can rename a file, change its reported content type, or upload content that the application was never designed to handle.
Checking whether a filename ends with .jpg or .pdf is not enough. A file called invoice.pdf could contain something entirely different from a PDF document.
A safer upload process checks the file extension, inspects its signature, enforces a size limit, generates a server-controlled filename, and stores the file outside the application's executable directory. Files that require additional inspection should remain unavailable to users until they pass the required security checks.
Why File Extensions Are Not Enough
A file extension is part of its name. It does not prove what the file contains.
For example, an attacker could rename a script to profile.jpg and submit it through an upload form. The browser may display the filename and report an image content type, but neither value proves that the content is a valid image.
ASP.NET Core exposes uploaded files through IFormFile. Its FileName and ContentType properties contain information supplied with the upload request. Treat that information as untrusted input.
Three checks serve different purposes:
Validation | What it checks | Limitation |
|---|
File extension | Whether the filename ends with an allowed extension | The extension can be changed |
Content type | The MIME type reported by the client | The client can supply a false value |
File signature | Whether the initial bytes match an expected file format | A matching signature does not prove the entire file is safe |
Use these checks together. For formats with complex internal structures, consider parsing the file with a trusted library and scanning it for malware before making it available to other users.
1. Set Up a File Upload Endpoint
For small files, ASP.NET Core can bind a multipart form upload to IFormFile.
The following example uses a Minimal API endpoint. It accepts image uploads and delegates validation and storage to a service.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<FileUploadService>();
var app = builder.Build();
app.MapPost("/upload", async (
[FromForm] IFormFile file,
FileUploadService uploadService,
CancellationToken cancellationToken) =>
{
var result = await uploadService.SaveImageAsync(
file,
cancellationToken);
return result.Success
? Results.Ok(new { fileId = result.FileId })
: Results.BadRequest(new { error = result.Error });
});
app.Run();
The endpoint receives the file and passes it to a dedicated service. This keeps validation and storage logic separate from HTTP request handling.
The service should enforce the application's upload rules. In a production application, also apply authentication and authorization where appropriate, and protect cookie-authenticated upload endpoints against cross-site request forgery.
The example uses a response containing a file identifier instead of returning a physical storage path to the client.
2. Validate the File Extension
Start with an allowlist of extensions that your application actually needs.
For example, a profile image service might accept JPEG and PNG files but reject everything else.
private static readonly HashSet<string> AllowedExtensions =
new(StringComparer.OrdinalIgnoreCase)
{
".jpg",
".jpeg",
".png"
};
private static bool HasAllowedExtension(string fileName)
{
var extension = Path.GetExtension(fileName);
return !string.IsNullOrWhiteSpace(extension)
&& AllowedExtensions.Contains(extension);
}
The Path.GetExtension method extracts the extension, and the allowlist rejects unsupported formats.
Using StringComparer.OrdinalIgnoreCase avoids treating .JPG and .jpg as different extensions.
This check is useful, but it is only the first step. Never accept a file simply because its name ends with an approved extension.
Also avoid using the original filename as a storage path. The filename comes from the client and may contain path information or unexpected characters.
3. Validate File Signatures
A file signature, sometimes called a magic number, is a sequence of bytes associated with a file format.
For example, a PNG file begins with these eight bytes:
89 50 4E 47 0D 0A 1A 0A
A JPEG file begins with the bytes FF D8 FF.
The following validator checks the signatures for JPEG and PNG files.
private static readonly Dictionary<string, byte[]> FileSignatures =
new(StringComparer.OrdinalIgnoreCase)
{
[".jpg"] = [0xFF, 0xD8, 0xFF],
[".jpeg"] = [0xFF, 0xD8, 0xFF],
[".png"] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
};
private static async Task<bool> HasValidSignatureAsync(
IFormFile file,
string extension,
CancellationToken cancellationToken)
{
if (!FileSignatures.TryGetValue(extension, out var signature))
{
return false;
}
if (file.Length < signature.Length)
{
return false;
}
var header = new byte[signature.Length];
await using var stream = file.OpenReadStream();
var bytesRead = await stream.ReadAtLeastAsync(
header,
signature.Length,
throwOnEndOfStream: false,
cancellationToken);
return bytesRead == signature.Length
&& header.AsSpan().SequenceEqual(signature);
}
The validator reads only the bytes needed for the signature check. It rejects files that are too short and checks the header against the expected byte sequence.
This is a format-specific check, not a complete image validation routine. An attacker can construct a file with a valid header followed by malformed or malicious content. For production use, decode the image with a maintained image-processing library and enforce reasonable dimensions and resource limits.
Do not reuse the JPEG or PNG signatures for PDF files, ZIP archives, or other formats. Each format needs its own validation rules.
4. Enforce File Size Limits
A user can upload a very large file even when the application only needs a small image. Processing too many large uploads can consume temporary disk space, memory, and network capacity.
Set an application-level limit:
private const long MaxFileSize = 5 * 1024 * 1024;
This example permits files up to 5 MiB.
Reject empty files and files that exceed the limit before processing them:
if (file.Length <= 0 || file.Length > MaxFileSize)
{
return UploadResult.Failure("Invalid file size.");
}
The application should also enforce request-size limits at the ASP.NET Core and hosting layers. An application-level check happens after the request has reached the endpoint, so it cannot replace limits that protect the server while receiving the request.
For small uploads, IFormFile is convenient. For large files or high upload volumes, consider streaming to reduce buffering demands.
5. Save Files Using Server-Generated Names
Never use the original filename to determine where an uploaded file is stored.
The following example generates a random storage name:
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
var storedFileName = $"{Guid.NewGuid():N}{extension}";
var uploadDirectory = Path.Combine(
app.Environment.ContentRootPath,
"uploads");
Directory.CreateDirectory(uploadDirectory);
var filePath = Path.Combine(uploadDirectory, storedFileName);
A generated filename avoids trusting the client's path or filename. However, the upload directory must still be configured safely.
In production, store uploads outside the application's directory tree, preferably in a dedicated storage location or object storage service. Disable execute permissions on the upload location, and do not serve uploaded files directly from an executable application directory.
If the application needs to display the original filename, store it separately as metadata and encode it appropriately when displaying it.
6. Put the Validation Steps Together
The service can combine the checks before saving a file.
public async Task<UploadResult> SaveImageAsync(
IFormFile file,
CancellationToken cancellationToken)
{
const long maxFileSize = 5 * 1024 * 1024;
if (file is null || file.Length <= 0)
{
return UploadResult.Failure("The file is empty.");
}
if (file.Length > maxFileSize)
{
return UploadResult.Failure("The file exceeds the size limit.");
}
var extension = Path.GetExtension(file.FileName);
if (!HasAllowedExtension(file.FileName))
{
return UploadResult.Failure("File extension is not allowed.");
}
if (!await HasValidSignatureAsync(
file,
extension,
cancellationToken))
{
return UploadResult.Failure("File signature is invalid.");
}
// Decode and validate the image with a maintained image library
// before making it available to users.
return await SaveValidatedImageAsync(
file,
extension,
cancellationToken);
}
Here, UploadResult and SaveValidatedImageAsync represent application-specific types and methods that must be implemented. The final storage step should generate a server-controlled filename and apply the storage policy described above.
The code illustrates the validation flow, but it is not a complete production-ready upload service until image decoding, storage, error handling, and any required malware scanning are implemented.
Common Mistakes to Avoid
Trusting ContentType
The uploaded MIME type is supplied by the client. Use it as an additional consistency check, not as proof of file safety.
Accepting every file extension
An allowlist is easier to maintain than a list of rejected extensions. Permit only formats the application needs.
Saving files inside wwwroot
Files stored in a public web directory may become directly accessible. Store untrusted uploads separately and expose them only through an endpoint that applies the required access checks.
Assuming a valid signature means a safe file
A valid header does not prove that the file is well-formed or free from malware. Decode supported formats and use a malware scanner where the application's risk requires it.
Checking size only in the controller
Request limits should also protect the server before the application processes the upload. Configure the request-size limits at the relevant hosting and framework layers.
Best Practices for Production Applications
Use a strict allowlist of required file formats.
Validate file size, extension, signature, and actual content.
Generate a unique storage name on the server.
Store files outside the application directory with execution disabled.
Apply authentication, authorization, and antiforgery protection where needed.
Scan files for malware when the application requires it.
Keep unscanned files quarantined until they pass inspection.
Limit request size and concurrent upload processing.
Log validation failures without logging sensitive file contents.
Test malformed files, empty files, oversized uploads, and mismatched extensions.
Advantages and Disadvantages
Advantages | Disadvantages |
|---|
Rejects many files that do not match the expected format | Signature checks alone cannot establish complete file safety |
Prevents unsupported file types from entering normal processing | Each supported format needs its own validation rules |
Server-generated names reduce path manipulation risks | Original filename metadata must be managed separately |
Size limits help control resource use | Large uploads may need a streaming implementation |
Quarantine and scanning add another security layer | Malware scanning introduces operational and processing costs |
Troubleshooting Common Problems
A valid image is rejected
Check whether the signature table includes the file's extension and expected header. Some formats have multiple valid signatures, so a validator may need to support additional variants.
The upload fails before reaching the endpoint
Check the request-size limits configured in ASP.NET Core, the reverse proxy, and the hosting platform. A request may be rejected before application-level validation runs.
The application cannot save the file
Confirm that the configured storage directory exists and that the application identity has the required write permissions. Do not solve the problem by granting broad permissions to the entire server.
Large uploads consume too many resources
Review buffering behavior and concurrent request volume. Use streaming for large files and consider background processing for expensive validation or malware scanning.
Summary
Secure file uploads in ASP.NET Core require several checks because filenames and client-reported content types can be misleading. Validate the extension, inspect the file signature, enforce size limits, and generate a server-controlled storage name.
For production applications, add content decoding, malware scanning where appropriate, safe storage permissions, and access controls. Keep untrusted files separate from executable application content and do not make them available to users before required checks are complete.
The goal is to reject files that do not meet the application's requirements before they enter normal processing. A layered validation process is more reliable than trusting a filename or a single signature check.
Jasen FiciPosted Oct 21, 2026, 7:21 AM
Thanks for sharing this. We included it in DotNetNews here: https://dotnetnews.beehiiv.com/p/new-post-1407