File Uploads & Handling
Letting users upload files — KYC documents, proofs of address, images — is necessary and dangerous. An upload is untrusted input. It can carry malware, be huge, pretend to be a type it is not, or try to escape into your filesystem. Validate strictly, store safely, and serve carefully.
Uploads bring several risks together: malicious content (malware, embedded scripts), resource abuse (huge files filling disk or memory), type tricks (an executable renamed to .pdf, or an image that is really HTML the browser runs), and path traversal (a filename like ../../etc/passwd). For us, uploads are also usually special-category KYC data, so they need strong protection at rest.
Treat every upload as hostile until proven otherwise. Check it at the trust boundary (see Trust Boundaries), store it apart from where it is served, and never trust the client-supplied name or type.
Validate the upload
- DoEnforce a maximum size and reject oversized uploads early. Stream files; do not buffer whole files in memory. This prevents resource exhaustion (see Performance & Resource Use).
- DoValidate file type by inspecting the content (magic bytes), not just the extension or the client-supplied content-type, and allow only an explicit list of expected types.
- DoGenerate your own safe storage name and path. Never use the user-supplied filename to build a path (path traversal). Sanitise it for display only.
- DoScan uploads for malware where you can, before they are processed or made available.
- NeverRun uploaded content, or let the browser run it. Store it as data, serve it with a safe content-type and
Content-Disposition: attachment, and never from a path that runs code.
Store and serve safely
- DoStore uploads in dedicated object/blob storage, separate from the application and not in a web-served directory (see File & Blob Storage).
- DoTreat KYC/identity uploads as special-category data: encrypt at rest, tight access, tenant isolation, and retention limits (see Data Classification, Data Protection & Privacy).
- DoServe files back through an authorised, access-checked endpoint or a short-lived signed URL, not a public, guessable link.
- ConsiderRemoving metadata (for example EXIF/GPS in images) that may leak more than the user intends.
- NeverServe user uploads from the same origin as the app without a safe content-type and a download disposition. A malicious HTML/SVG upload can run as script in the user's session (stored XSS).
var path = Path.Combine("uploads", file.FileName); // ../../ escapes
File.WriteAllBytes(path, bytes);
// later served inline from /uploads with the original type
Path traversal through the filename, no size or type validation, and serving inline. An uploaded .html or .svg file then runs as script for whoever views it. Three classic upload vulnerabilities at once.
if (bytes.Length > MaxSize) return TooLarge();
if (!IsAllowedType(Sniff(bytes))) return Unsupported();
var blobName = Guid.CreateVersion7().ToString(); // our name, not theirs
await blob.UploadAsync(container, blobName, bytes); // isolated storage
// served via an authorised endpoint with Content-Disposition: attachment
Size and real type are checked, the storage name is ours (no traversal), files live in separate storage, and they are served as downloads, not as content that can run.
Self-review checklist
- AskIs the size limited and the type validated by content, not just by extension?
- AskAm I using the user's filename to build a path anywhere?
- AskCould an uploaded file run, or run as script, when it is served back?
- AskIs this (likely KYC) file encrypted, access-controlled, and tenant-isolated at rest?