Insights / Security
File upload security for business applications: the go-live checklist
Before you ship an upload feature, run it through this checklist: authenticate the request, validate bytes server-side, isolate parsing, scan asynchronously, store files private, sign every download, and log it all.
Kiran Bandarupalli · 23 Oct 2026 · 5 min read

Every business application eventually grows an upload button. Invoices, ID proofs, resumes, contracts, medical records — the files that make a system genuinely useful are exactly the files attackers love to poison. An upload endpoint accepts untrusted bytes from the internet and hands them to your infrastructure, so it deserves more design attention than it usually gets. This is the checklist we run against every upload feature in the custom software development projects we ship — from first-byte authentication to audit-ready logging.
Authenticate and authorize before the first byte
Your upload endpoint is not a public form. Require a session or API token on the upload URL itself, and check that this specific user may attach this specific file to this specific record. Tenant isolation matters: a user from Company A should never be able to attach a file to Company B's invoice, even if they guess the right IDs. Where uploads arrive from automated clients, issue short-lived credentials — presigned POST policies from your storage provider — instead of handing out broad API keys.
Validate on the server — the browser is not a security boundary
Client-side validation is a courtesy to honest users; it stops nobody else. Every check must be re-applied server-side:
- Size limits. Enforce them before buffering the whole file — stream the request and abort when the cap is exceeded, or a 2 GB "profile picture" becomes a free denial of service.
- MIME sniffing, not file extensions. An
.exerenamed to.jpgis still an executable. Read the magic bytes of the first chunk and accept only a whitelist of real types. - Filename sanitization. Treat the original filename as a display label, never as a path or a command argument. Strip directory traversal, null bytes, and anything outside a safe character set.
- Content shape. If you expect a PDF, parse it as a PDF. Re-encoding images through a decoder strips embedded scripts and malformed payloads that slip past simple type checks.
The validation core looks like this in Node.js:
import { fileTypeFromBuffer } from 'file-type';
// Never trust the extension or the Content-Type header.
const ALLOWED = new Set(['image/jpeg', 'image/png', 'application/pdf']);
const MAX_BYTES = 10 * 1024 * 1024;
async function acceptUpload(buffer, claimedName) {
if (buffer.length > MAX_BYTES) throw new Error('file too large');
const detected = await fileTypeFromBuffer(buffer); // magic-byte sniffing
if (!detected || !ALLOWED.has(detected.mime)) {
throw new Error('type not allowed: ' + (detected ? detected.mime : 'unknown'));
}
// The original name is a display label, never a path.
const safeName = claimedName.replace(/[^a-zA-Z0-9._-]/g, '_');
return { mime: detected.mime, safeName };
}
Isolate processing from the application
Parsing a file is one of the most dangerous things your app does: image decoders, PDF renderers, and archive extractors all parse hostile input in native code with a long history of memory-safety bugs. Keep that work out of the request path: stream the bytes to object storage, hand a reference to an isolated worker, and never let your API process deserialize an uploaded file in-process. The full pattern — presigned upload, quarantined bucket, async processing — is what we describe in our secure file-upload pipeline, and it applies unchanged whether you run on a VPS or a managed cloud.
Scan everything before it becomes trusted
Antivirus scanning is a layer, not the strategy — but it is the layer auditors ask about first. Wire it in as an asynchronous step between upload and availability:
- Quarantine first. Files land in a private bucket that no download path can reach.
- Scan in a worker with a timeout and a failure policy. A scanner that hangs is an outage, so bound it.
- Accept verdicts over webhooks. Make webhook consumers safe to retry so a late or duplicate scanner callback cannot flip a verdict backwards.
- Fail closed. Hold the file in "pending" until a verdict arrives. If the scanner times out, keep the file quarantined and page someone — never default to "clean".
- Record the verdict. Store the scan result, scanner version, and timestamp with the file metadata. Auditors will ask, and you will want to deploy database changes without a maintenance window when that metadata schema grows.
Scan queues are asynchronous workflows with their own failure modes, so give them the same observability for asynchronous workflows you would give any background pipeline: queue depth, scan latency, and verdict outcomes on a dashboard.
Store private by default, deliver through signed URLs
Buckets hold files; URLs decide who sees them. Make every bucket private, serve nothing from predictable paths, and issue signed URLs with minutes-long expiry after checking authorization on each request — the design we laid out for signed URLs, expiry, and access control on the download side. The rule is simple: possession of a URL should never equal permanent access to a business document.
Log it like an auditor will read it
Uploads are a compliance surface. For every file, record who uploaded it, when, the byte size, the detected MIME type, the storage reference, and the scan verdict. Keep those events immutable and separate from application logs. When a customer asks "who had access to this contract in March?", the answer should be a query, not an archaeology project.
The go-live checklist
- Upload endpoint requires authentication and per-record authorization.
- Size caps enforced while streaming, before buffering.
- Magic-byte type whitelist; extensions ignored for decisions.
- Filenames sanitized; never used as paths.
- Files parsed only in isolated workers, never in the API process.
- Quarantine bucket; no direct serving of raw uploads.
- Antivirus scan with timeouts, retries, and a fail-closed default.
- Private storage; signed, expiring download URLs.
- Per-request authorization on every download.
- Upload, scan verdict, and access events logged immutably.
- Webhook consumers idempotent for scanner callbacks.
- Metadata schema versioned; zero-downtime migrations planned.
Most upload incidents are not zero-days — they are a skipped check on this list. If you are building a document-heavy application and want this hardened from day one, our custom software development team builds it into the design, and we operate uploads under the same discipline as the rest of our cloud and DevOps work. Tell us about your project and we will walk through the checklist together.