File Upload Basics & Hardening
On this page
File Upload Is an Attack Surface
File uploads combine multiple risks: - large memory allocations - disk exhaustion - path traversal - malware distribution - content-type spoofing - stored XSS (SVG, HTML) - SSRF via image processors If you treat file upload as “just accept IFormFile and save it”, you will ship a security incident. Production rule: Uploads must be streamed, validated, isolated, and scanned.Real Production Incident
Symptoms: - Application crashes with OOM during peak usage. - Disk fills up unexpectedly. - Security report: malicious file served from your domain. - CDN flagged for distributing malware. Root cause: - Files buffered entirely in memory. - No file size limit. - Uploaded files stored under web root. - No extension allowlist or content validation. - No virus scanning. This is a multi-layer failure, not just “big files”.Symptom → Cause → Diagnosis → Fix
Symptom: - Memory spikes on upload endpoints. - High GC pressure. - Unexpected files accessible under static paths. - Large spikes in disk usage. Cause: - Copying file to MemoryStream. - No request size limits. - Saving user files under wwwroot. - Trusting file extension or Content-Type header. Diagnosis: - Inspect memory allocation during uploads. - Check where files are stored on disk. - Attempt upload with renamed .exe as .jpg. - Review request size and multipart limits. Fix: - Stream directly to disk or object storage. - Enforce request and file size limits. - Store uploads outside static root. - Validate extension and inspect content. - Integrate malware scanning.Anti-Pattern: Buffering Entire File in Memory
This is dangerous under load:
app.MapPost("/upload", async (IFormFile file) =>
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
});
If 100 users upload 20MB files concurrently:
- 2GB memory spike
- OOM and restart
- Partial writes
Production rule:
Never load full file into memory unless extremely small and bounded.
Correct Pattern: Stream to Disk or External Storage
Stream directly:
app.MapPost("/upload", async (HttpContext ctx) =>
{
var file = ctx.Request.Form.Files[0];
var uploadsRoot = "/app/uploads";
Directory.CreateDirectory(uploadsRoot);
var safeName = Path.GetRandomFileName();
var path = Path.Combine(uploadsRoot, safeName);
await using var stream = File.Create(path);
await file.CopyToAsync(stream, ctx.RequestAborted);
return Results.Ok();
});
Important:
- Do not trust original filename.
- Generate your own server-side name.
- Store metadata separately if needed.
Request Size and Multipart Limits
Without size limits: - attackers can exhaust memory/disk - large payloads degrade performance Enforce limits at multiple levels: - Kestrel limits - Reverse proxy limits - Endpoint-level validation Conceptually:
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
});
And validate file.Length explicitly.
Production rule:
Size limits must exist at network and application layer.
Extension and Content Validation
Never trust: - file extension - Content-Type header Attack: Upload malware.exe renamed to image.jpg. Mitigation: - Maintain allowlist of extensions. - Inspect file signature (magic numbers) for critical types. - Reject double extensions (file.jpg.exe). Even better: - Do not execute or directly serve uploaded files. - Store and serve via safe object storage with controlled headers.Serving Uploaded Files Safely
If you serve uploads: - isolate directory from app assets - enforce Content-Disposition: attachment for untrusted files - set X-Content-Type-Options: nosniff - do not allow direct execution Never: Store uploads under wwwroot and serve via UseStaticFiles blindly.Malware Scanning
For public-facing uploads: - integrate AV scanning pipeline - mark file as “quarantined” until scanned - do not make file accessible before scan completes Production rule: Security scanning must not block request thread excessively. Offload scanning asynchronously if needed, but control access until safe.Path Traversal and Filename Risks
Never use user-provided filename directly: Bad:
var path = Path.Combine("/uploads", file.FileName);
If FileName contains path separators:
- ../ sequences
- absolute paths
You risk writing outside intended directory.
Correct:
- ignore original filename
- generate random filename
- normalize and verify resolved path stays under intended root