Validating file uploads properly
There are four different things people mean by "validating an upload", and they protect against different problems. Getting them in the right order — and in the right place — is most of the work.
Two places, two purposes
Client-side validation exists to make the product pleasant: the user learns instantly that a 900 MB video will not be accepted, instead of after a four-minute upload. Server-side validation exists to make the product safe. They are not alternatives, and the client's verdict is never authoritative.
Rule of thumb: if removing the browser from the equation would let something bad through, the check belongs on the server too. Attackers post directly to your endpoint.
1. Extension allowlists
The first and most valuable check. Always an allowlist — a blocklist is a list of the extensions you happened to think of, and it will not include the next one.
<core-upload asp-extensions=".jpg,.jpeg,.png,.pdf"
asp-max-size="25MB" />
// and, authoritatively, on the server:
builder.Services.AddCoreUpload(o =>
{
o.AllowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".pdf" };
o.MaxFileSizeBytes = 25 * 1024 * 1024;
}); Two details that catch people out: check the last extension (photo.jpg.aspx is an .aspx file), and compare case-insensitively.
2. MIME type — useful, not trustworthy
The browser guesses a Content-Type from the extension and the OS registry, and an attacker sets it to whatever they like. Treat it as a hint that improves UX, never as proof of what a file contains.
One practical note specific to chunked uploads: a chunked request carries no meaningful per-chunk content type, so a MIME allowlist can only be enforced where a content type actually exists. Extension and magic-byte checks are the enforcement points on that path.
3. Magic bytes: what the file really is
Most formats start with a fixed signature. Reading the first handful of bytes tells you what you actually received, regardless of the name:
| Format | Leading bytes | As text |
|---|---|---|
| JPEG | FF D8 FF | — |
| PNG | 89 50 4E 47 | .PNG |
| GIF | 47 49 46 38 | GIF8 |
25 50 44 46 | %PDF | |
| ZIP / DOCX / XLSX | 50 4B 03 04 | PK.. |
| WebP | 52 49 46 46 … 57 45 42 50 | RIFF…WEBP |
Enable the client-side check with a single option; it reads the first bytes in the browser and rejects mismatches before any bytes go over the wire:
CoreUpload.create('#uploader', {
allowedExtensions: '.jpg,.png,.pdf',
validateMimeByMagic: true // read the signature, not the name
}); Know the limit: a signature check proves the file starts like a PNG. A polyglot can be a valid PNG and a valid HTML document at once. For untrusted images shown to other users, re-encoding server-side is the only check that truly normalizes content.
4. Size — declared versus actual
Checking a size before the transfer saves everyone time. Enforcing one while writing is what protects the disk, because the declared size is a number the client made up. Both matter, and the second is not optional:
- Client:
asp-max-size/maxFileSizegives an instant, friendly rejection. - Server:
MaxFileSizeBytesis enforced as bytes arrive, and the framework's own request-size limits still apply. - Also set a minimum where it makes sense — a 0-byte "document" is usually a bug worth surfacing.
5. Image rules: dimensions and aspect ratio
For avatars, product photos and banners the useful constraints are pixel dimensions and shape, and both can be checked in the browser before uploading:
<core-upload asp-min-width="200" asp-min-height="200"
asp-max-width="6000" asp-max-height="6000"
asp-crop="true" asp-crop-aspect-ratio="1:1" /> Maximum dimensions are a security control as much as a product rule: decoding a 25,000 × 25,000 image allocates gigabytes. Clamp before you decode.
6. Duplicates, by content not by name
Two files with the same name may differ; two files with different names may be identical. Hash the content to know which:
<core-upload asp-compute-hash="true" asp-hash-algorithm="sha256" /> Hashing runs in a Web Worker so a large file does not freeze the page, and the digest reaches the server with the upload — useful for de-duplication, content-addressable storage and integrity checks.
7. Custom rules and the ordering that matters
Business rules ("invoices must be PDFs under 5 MB and named with a client code") belong in a custom validator. Put the cheap checks first: reject on extension before you read bytes, read bytes before you decode an image, and decode before you call an antivirus service. Each stage is more expensive than the last, and every file you reject early is work you never do.
Where each check belongs
| Check | Client | Server | Why |
|---|---|---|---|
| Extension | Yes (UX) | Required | Primary defense against executable uploads |
| Size | Yes (UX) | Required | Declared size is a client claim |
| Magic bytes | Yes (fast reject) | Recommended | Catches renamed files |
| Dimensions | Yes | If you decode | Pixel bombs allocate memory |
| Hash / duplicates | Yes | If authoritative | Client hash is unverified |
| Antivirus | No | For redistributed files | Only the server can be trusted to run it |