DOTNET Contents

File Upload Basics & Hardening

File uploads are a high-risk boundary: memory pressure, path traversal, malware, content-type spoofing, and storage abuse. Learn how to stream safely, isolate storage, and harden upload endpoints.

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

Security Notes

Uploads can be used for: - DoS (large files) - stored XSS (SVG with scripts) - malware hosting - SSRF (image processors fetching remote URLs inside file metadata) - content smuggling Mitigations: - strict size limits - extension allowlist - content inspection - no inline rendering of untrusted content - rate limiting on upload endpoints

Operational Notes

Monitoring: - Track upload size distribution. - Alert on unusual spikes in upload traffic. - Monitor disk usage of upload storage. - Track upload-related 400/413 errors. Rollout: - Test upload under concurrent load. - Validate reverse proxy and Kestrel limits align. - Verify storage directory permissions. Rollback: - If upload regression causes OOM or disk exhaustion, disable endpoint quickly. - Rotate storage if malicious files were stored. - Audit access logs if sensitive content exposed.

Checklist

- Files are streamed, not buffered entirely in memory. - Max request body size enforced at multiple layers. - Uploaded files stored outside static/web root. - Original filenames are not trusted for storage paths. - Extension allowlist and content inspection implemented. - Uploaded content not executed or rendered inline unsafely. - Malware scanning strategy defined for public uploads. - Upload metrics and disk usage monitored. - Upload endpoint protected by auth and/or rate limiting as appropriate.