ASP.NET Core MVC · Production

File Uploads

Sellers on ShopSmart upload product photos. Uploads are one of the riskiest features a web app has: huge files, fake extensions, malicious names like ../../web.config. This lesson accepts files the safe way.

Advanced 7 min Lesson 34 of 42Examples target .NET 9.
Before you start:Model BindingSecurity
0/42 in ASP.NET Core MVC
On this page · 7 min · Advanced

In simple words#

Accepting uploads is like accepting parcels at a gate: check size, type and contents, give it a new safe label, and store it away from the public area.

IFormFile
An uploaded file.
Validate
Size, extension and content type.
Safe name
Generate your own file name.

Learning objectives#

  • Receive files with IFormFile from a multipart form
  • Limit request and file sizes
  • Validate the extension and the actual file content
  • Store files under random names outside wwwroot

Explanation#

The form needs method="post" and enctype="multipart/form-data". The action receives each file as an IFormFile (or IFormFileCollection for several) with FileName, Length, ContentType and OpenReadStream().

Limit sizes twice: [RequestSizeLimit] (or Kestrel/FormOptions limits) rejects oversized requests early, and your own check gives a friendly message for files that are too big.

Never trust the file name or ContentType — the client chooses both. Check an allow-list of extensions and the file signature (“magic bytes”): PNG files always start with 89 50 4E 47 0D 0A 1A 0A.

Generate your own file name (a GUID plus the validated extension) and save outside wwwroot, e.g. App_Data/uploads, so uploaded files can never be executed or served without your code deciding. Serve them through an action that checks permissions.

For large files, stream them instead of buffering in memory, and consider storing them in cloud storage (Azure Blob Storage, Amazon S3) with a virus scan step.

Syntax#

C# Illustrative
[HttpPost, ValidateAntiForgeryToken, RequestSizeLimit(5 * 1024 * 1024)]
public async Task<IActionResult> Create(IFormFile photo)
{
    var storedName = 
quot;
{Guid.NewGuid():N}.png"; // never use photo.FileName as the path
await using var target = System.IO.File.Create(Path.Combine(uploadsFolder, storedName)); await photo.CopyToAsync(target); }

Code example: A product-photo upload that checks everything#

C#Controllers/UploadsController.cs Illustrative
// Program.cs
using System.Text.RegularExpressions;


var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                   // keep the demo output clean
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapDefaultControllerRoute();
app.Urls.Add("http://127.0.0.1:5120");
await app.StartAsync();


using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5120") };
var form = await http.GetStringAsync("/Uploads/Create");
var token = Regex.Match(form, "__RequestVerificationToken\" type=\"hidden\" value=\"([^\"]+)").Groups[1].Value;


async Task Upload(string label, string fileName, byte[] bytes)
{
    using var content = new MultipartFormDataContent();
    content.Add(new StringContent(token), "__RequestVerificationToken");
    content.Add(new StringContent("Mouse, top view"), "caption");
    content.Add(new ByteArrayContent(bytes), "photo", fileName);
    var response = await http.PostAsync("/Uploads/Create", content);
    Console.WriteLine(
quot;
{label,-18} {(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
} byte[] pngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; await Upload("real PNG", "mouse.png", [.. pngSignature, .. new byte[2_000]]); await Upload("renamed .exe", "photo.png", "MZ - this is really a program"u8.ToArray()); await Upload("wrong extension", "manual.pdf", [.. pngSignature, .. new byte[100]]); await Upload("3 MB image", "big.png", [.. pngSignature, .. new byte[3 * 1024 * 1024]]); await app.StopAsync(); Directory.Delete(Path.Combine(app.Environment.ContentRootPath, "App_Data"), recursive: true); // tidy up the demo // Controllers/UploadsController.cs using Microsoft.AspNetCore.Mvc; public class UploadsController(IWebHostEnvironment env) : Controller { private const long MaxBytes = 2 * 1024 * 1024; private static readonly byte[] PngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; public IActionResult Create() => View(); [HttpPost, ValidateAntiForgeryToken] [RequestSizeLimit(5 * 1024 * 1024)] // reject huge requests before binding public async Task<IActionResult> Create(IFormFile? photo, string caption) { if (photo is null || photo.Length < PngSignature.Length) return BadRequest("Choose an image file"); if (photo.Length > MaxBytes) return BadRequest(
quot;Images can be at most 2 MB (this one is
{photo.Length / 1024 / 1024} MB)");
if (!string.Equals(Path.GetExtension(photo.FileName), ".png", StringComparison.OrdinalIgnoreCase)) return BadRequest("Only .png images are allowed"); // Check the content, not just the name: every PNG starts with the same 8 bytes var header = new byte[PngSignature.Length]; await using (var stream = photo.OpenReadStream()) await stream.ReadExactlyAsync(header); if (!header.SequenceEqual(PngSignature)) return BadRequest("The file is not really a PNG image"); var folder = Path.Combine(env.ContentRootPath, "App_Data", "uploads"); // outside wwwroot Directory.CreateDirectory(folder); var storedName =
quot;
{Guid.NewGuid():N}.png"; // never trust the client's file name
await using (var target = System.IO.File.Create(Path.Combine(folder, storedName))) await photo.CopyToAsync(target); return Content(
quot;Saved '
{caption}' ({photo.Length} bytes) under a random name");
} } @* Views/Uploads/Create.cshtml *@ <form asp-action="Create" method="post" enctype="multipart/form-data"> <input type="file" name="photo" accept="image/png" /> <input name="caption" placeholder="Caption" /> <button>Upload</button> </form>
Expected output
real PNG           200 Saved 'Mouse, top view' (2008 bytes) under a random name
renamed .exe       400 The file is not really a PNG image
wrong extension    400 Only .png images are allowed
3 MB image         400 Images can be at most 2 MB (this one is 3 MB)

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The form uses enctype="multipart/form-data"; the antiforgery token travels as a normal form field.
  2. The real PNG passes every check and is saved under a GUID name in App_Data/uploads.
  3. The renamed program has a .png name, but its first bytes are “MZ…”, not the PNG signature — rejected.
  4. A PDF name fails the extension allow-list before the content is even read.
  5. The 3 MB file is under the 5 MB request limit, so it binds, but the 2 MB rule rejects it with a clear message.

Real-world use case#

ShopSmart sellers upload product photos and PDF manuals. Files are validated like this, then copied to Azure Blob Storage under random names; product pages show them through a CDN URL.

Common mistakes#

Using the uploaded file name as the path

A name like ..\..\appsettings.json or shell.aspx can overwrite or plant files. Generate your own names.

Saving into wwwroot

Anything in wwwroot is publicly downloadable — including an uploaded .html with scripts. Store elsewhere and serve deliberately.

Trusting ContentType

photo.ContentType is whatever the client claims. Check the bytes.

Best practices#

  • Size limits on the request and the file
  • Extension allow-list plus signature check
  • Random names, storage outside wwwroot
  • Scan files and use cloud storage at scale

Remember this#

enctype="multipart/form-data" → IFormFile.

Never trust FileName or ContentType — check the bytes.

Random name, outside wwwroot.

Practice exercise#

Your task: Add a Download(string name) action that returns an uploaded file only if name matches the pattern of 32 hex characters plus .png, preventing path traversal.

Need a hint?
Validate with Regex.IsMatch(name, "^[a-f0-9]{32}\\.png
quot;), then return PhysicalFile(fullPath, "image/png").
Show solution
C# Illustrative
// Controllers/UploadsController.cs
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;


public class UploadsController(IWebHostEnvironment env) : Controller
{
    public IActionResult Download(string name)
    {
        if (!Regex.IsMatch(name, "^[a-f0-9]{32}\\.png
quot;
)) return BadRequest();
var path = Path.Combine(env.ContentRootPath, "App_Data", "uploads", name); return System.IO.File.Exists(path) ? PhysicalFile(path, "image/png") : NotFound(); } }

Mini quiz#

File Uploads quizMultiple choice Question 1 of 3

Which form attribute is required for file uploads?

Score so far: 0/0

Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.