close
Direct-to-S3 / Azure tus 1.0 resume IndexedDB Golden Retriever 30 locales Webcam & screen capture

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
PDF 25 50 44 46 %PDF
ZIP / DOCX / XLSX 50 4B 03 04 PK..
WebP 52 49 46 4657 45 42 50 RIFFWEBP

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 / maxFileSize gives an instant, friendly rejection.
  • Server: MaxFileSizeBytes is 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