File uploads require careful attention to security — malicious files can execute server-side code or consume excessive storage. The ASP.NET Core file upload pipeline validates input at multiple layers: file extension (weak — easily spoofed), magic bytes (the actual file signature — stronger), and size limits (prevents disk exhaustion). Images are resized server-side to standardise storage and prevent enormous originals from being stored. The final URL is returned to the client for storage in the database.
File Upload API
// ── Program.cs — configure upload limits ─────────────────────────────────
builder.Services.Configure<FormOptions>(opts =>
{
opts.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10MB max
});
builder.WebHost.ConfigureKestrel(opts =>
{
opts.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
});
// ── UploadsController ─────────────────────────────────────────────────────
[ApiController, Route("api/uploads"), Authorize]
public class UploadsController : ControllerBase
{
private readonly IBlobStorageService _storage;
private readonly ILogger<UploadsController> _logger;
private static readonly HashSet<string> AllowedMimeTypes =
["image/jpeg", "image/png", "image/webp", "image/gif"];
private static readonly Dictionary<string, byte[]> MagicBytes = new()
{
["image/jpeg"] = [0xFF, 0xD8, 0xFF],
["image/png"] = [0x89, 0x50, 0x4E, 0x47],
["image/webp"] = [0x52, 0x49, 0x46, 0x46], // RIFF header
["image/gif"] = [0x47, 0x49, 0x46], // GIF
};
[HttpPost("images")]
[RequestSizeLimit(10 * 1024 * 1024)]
public async Task<ActionResult<UploadResult>> UploadImage(
IFormFile file, CancellationToken ct)
{
// ── Validate file size ────────────────────────────────────────────
if (file.Length == 0) return BadRequest("File is empty.");
if (file.Length > 5_000_000) return BadRequest("File must be under 5MB.");
// ── Validate content type ─────────────────────────────────────────
if (!AllowedMimeTypes.Contains(file.ContentType.ToLower()))
return BadRequest("Only JPEG, PNG, WebP and GIF images are allowed.");
// ── Validate magic bytes (prevent extension spoofing) ─────────────
using var stream = file.OpenReadStream();
var header = new byte[8];
await stream.ReadExactlyAsync(header, 0, 8, ct);
stream.Position = 0; // reset for later use
var mimeOk = MagicBytes.Any(kvp =>
header.Take(kvp.Value.Length).SequenceEqual(kvp.Value) &&
kvp.Key == file.ContentType.ToLower());
if (!mimeOk)
return BadRequest("File content does not match the declared type.");
// ── Resize with ImageSharp ────────────────────────────────────────
// Install: dotnet add package SixLabors.ImageSharp
using var image = await Image.LoadAsync(stream, ct);
image.Mutate(x => x.Resize(new ResizeOptions {
Size = new Size(1200, 0), // max width 1200px, height auto
Mode = ResizeMode.Max, // only shrink, never enlarge
}));
using var output = new MemoryStream();
await image.SaveAsWebpAsync(output, new WebpEncoder
{ Quality = 82 }, ct); // compress to WebP quality 82
output.Position = 0;
// ── Store the file ────────────────────────────────────────────────
var fileName = $"{Guid.NewGuid():N}.webp";
var publicUrl = await _storage.UploadAsync(
container: "images",
fileName: fileName,
content: output,
mimeType: "image/webp",
ct: ct);
return Ok(new UploadResult(publicUrl, fileName));
}
}
public record UploadResult(string Url, string FileName);
Content-Type header — both of which are trivially spoofed by renaming a file or changing the request. A JPEG always starts with FF D8 FF; a PNG with 89 50 4E 47. An attacker cannot fake these bytes and still have a valid image that browsers render. However, magic byte validation alone is not sufficient against all attacks — pair it with server-side processing (ImageSharp decoding) which will throw an exception if the content is not a valid image.ImageSharp‘s WebpEncoder with quality 80-85 for a good balance between file size and visual quality.exploit.aspx (or renames it to match a valid image extension but containing server-side script) should not be able to execute it by navigating to the file URL. Store files with GUID names (not original filenames), use a separate blob storage service (Azure Blob) or serve through a dedicated endpoint that explicitly sets the correct content type.Common Mistakes
Mistake 1 — Trusting file extension or Content-Type header (easily spoofed)
❌ Wrong — if (file.ContentType == "image/jpeg"); attacker renames malware.exe to photo.jpg and changes Content-Type.
✅ Correct — check magic bytes from the file content; also process the image through ImageSharp (invalid images throw on decode).
Mistake 2 — Storing files with original filenames (path traversal and overwrite attacks)
❌ Wrong — saving as file.FileName; attacker uploads file named ../../web.config; overwrites server config.
✅ Correct — always generate a new GUID filename: $"{Guid.NewGuid():N}.webp"; ignore the original filename entirely.