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.
On this page · 7 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Advanced
- Updated
- Oct 29, 2026
Related tutorials
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#
[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#
// 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>
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#
- The form uses enctype="multipart/form-data"; the antiforgery token travels as a normal form field.
- The real PNG passes every check and is saved under a GUID name in App_Data/uploads.
- The renamed program has a .png name, but its first bytes are “MZ…”, not the PNG signature — rejected.
- A PDF name fails the extension allow-list before the content is even read.
- 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#
A name like ..\..\appsettings.json or shell.aspx can overwrite or plant files. Generate your own names.
Anything in wwwroot is publicly downloadable — including an uploaded .html with scripts. Store elsewhere and serve deliberately.
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?
Show solution
// 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}\\.pngquot;)) 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#
Which form attribute is required for file uploads?
Want more? Practise all ASP.NET Core MVC MCQs · ASP.NET Core MVC interview questions
My notes#
Saved in this browser.