Works with any backend npm install Zero dependencies

Upload security for JavaScript apps

A client library can make uploads pleasant and it can avoid introducing holes of its own. It cannot make your endpoint safe. This page is explicit about which half is which.

The uncomfortable truth: every check the browser performs can be skipped by an attacker, because an attacker does not use your page. Client validation is a user-experience feature. Server validation is the security control. A library that claims otherwise is selling you something.

What your server must do, regardless of library

  • Allowlist extensions server-side. Not a blocklist. Reject everything not on the list.
  • Store outside the web root, under a name you generated. If an uploaded file cannot be executed and cannot be addressed by its original name, two whole classes of attack disappear.
  • Enforce a size cap while writing, not just against the declared size — the declared size is a client claim.
  • Never build a path from client text. A file name of ../../config is a valid string; so is a chunk session id that contains a slash.
  • Decide who may read the file back. An unguessable URL is a capability, not authorization — fine for some products, not for others, but it should be a decision rather than an accident.

What the browser side can genuinely do

Three things, and they are worth having:

  • Fail fast. Rejecting a 900 MB file or a .exe before the transfer saves the user minutes and saves you bandwidth.
  • Check magic bytes. Reading the first bytes catches a renamed file locally — an honest-mistake filter, not a security boundary.
  • Not introduce its own vulnerabilities. Which is the part people never audit, and the subject of the rest of this page.

File names are attacker-controlled strings

This is the one that catches upload libraries themselves. A file name is text chosen by whoever made the file, it may legally contain <, > and quotes on Linux and macOS, and a hostile page can hand a user a file with any name at all through drag-and-drop. Your uploader then displays that string in a queue, a tooltip, a toast and an error message.

Two failure modes are common enough to name:

// WRONG - a file named <img src=x onerror=alert(1)>.png executes
row.innerHTML = '<span>' + file.name + '</span>';

// ALSO WRONG - escaping only & < > leaves quotes live, so a name like
//   a" onmouseover="steal()" x=".png
// breaks out of the attribute
el.innerHTML = '<div title="' + escapeAngleBrackets(file.name) + '">';

// RIGHT - let the DOM do it
const span = document.createElement('span');
span.textContent = file.name;
row.appendChild(span);

The same applies to server error text rendered back into the page, and to any metadata a user typed that is redisplayed after a re-render. If you are auditing an upload UI, grep for innerHTML and check every interpolation — including the ones inside attributes.

Content types lie in both directions

The Content-Type the browser attaches is derived from the extension and the OS registry, and any client can set it to anything. Equally, do not echo a stored content type back when serving a file unless you determined it yourself — that turns an upload into a stored-XSS vector on your own origin. Send X-Content-Type-Options: nosniff, prefer Content-Disposition: attachment for untrusted files, and serve user content from a separate origin when you can.

If you upload straight to cloud storage

Presigned uploads move the trust boundary to whoever signs. Three rules:

  • The signing endpoint is privileged — anyone who can call it can write into your bucket. Authenticate it.
  • Derive the object key on the server. If the client names the key, the client chooses where the object lands.
  • Bind limits into the signature (content-length range, short expiry). Validating a client's claims before signing does not constrain what is actually uploaded afterwards.

Resource limits are a security control

Denial of service through uploads rarely looks like an attack — it looks like the disk filling up. Cap bytes while writing, sweep abandoned chunk sessions on a timer, clamp image dimensions before decoding (a small file can declare enormous dimensions), and cap archive expansion if you extract anything. If your server proxies remote files, cap those too and honor backpressure so a slow reader cannot pin the whole file in memory.

This is a practical starting point, not a compliance document. If you handle regulated data, get the integration reviewed properly.