Skip to main content
Security7 min readOctober 28, 2025

Secure File Upload: Preventing Common Attack Vectors

File upload is one of the most dangerous features you can build. Here's how to implement it safely — from validation and storage to serving uploaded content.

James Ross Jr.
James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Secure File Upload: Preventing Common Attack Vectors

File upload is one of the most dangerous features in any web application. Every uploaded file is untrusted input from an external source, and unlike form fields that contain text, uploaded files can contain executable code, malware, or content designed to exploit your server, your storage infrastructure, or your users.

I have audited applications where the file upload endpoint was the single most critical vulnerability — accepting any file type, storing it in a publicly accessible directory on the web server, and serving it with the original filename and content type. That is not a file upload feature. It is an invitation to remote code execution.

Here is how to build file upload correctly.

Validation: What to Check Before Storing Anything

Never trust the file extension or the Content-Type header sent by the client. Both are trivially spoofable. An attacker can rename a PHP shell to profile.jpg and set the Content-Type to image/jpeg. If your validation checks only these values, the malicious file passes.

Validate the file's magic bytes. Every file format has a signature — a specific byte sequence at the beginning of the file that identifies its type. JPEG files start with FF D8 FF. PNG files start with 89 50 4E 47. PDF files start with 25 50 44 46. Read the first bytes of the uploaded file and verify they match the expected format. This is harder to spoof than extensions or headers.

Enforce file size limits at multiple layers. Set limits in your web server configuration, your application framework, and your upload handling code. A 10MB limit in your Express middleware does not help if nginx is configured to accept 100MB requests and buffers the entire file in memory before forwarding it.

Restrict allowed file types to the minimum your feature requires. If your application needs profile photos, accept JPEG and PNG only. Do not accept SVG — it can contain embedded JavaScript. Do not accept GIF unless you specifically need animation. Every additional file type you accept is additional attack surface.

const ALLOWED_TYPES: Record<string, Buffer> = {
 "image/jpeg": Buffer.from([0xff, 0xd8, 0xff]),
 "image/png": Buffer.from([0x89, 0x50, 0x4e, 0x47]),
 "application/pdf": Buffer.from([0x25, 0x50, 0x44, 0x46]),
};

Function validateFileType(buffer: Buffer, declaredType: string): boolean {
 const expectedMagic = ALLOWED_TYPES[declaredType];
 if (!expectedMagic) return false;
 return buffer.subarray(0, expectedMagic.length).equals(expectedMagic);
}

Scan for malware. For applications that accept documents, spreadsheets, or other complex file types, integrate a malware scanning service — ClamAV is a solid open-source option. Scan files before storing them. Quarantine files that fail scanning and alert your security team.

For a broader view of input validation and injection prevention, the XSS prevention guide covers related patterns for handling untrusted content.

Storage: Where and How to Keep Uploaded Files

Never store uploaded files in your web server's document root. If an attacker uploads a file containing server-side code and that file is accessible via a URL, the web server may execute it. This is how web shells are deployed. Store uploaded files in a location that is not served by your web server.

Use object storage. Services like Cloudflare R2, AWS S3, or Google Cloud Storage are purpose-built for file storage. They do not execute uploaded content. They provide access control, versioning, and lifecycle management. Configure your storage bucket to be private by default, and generate signed URLs when users need to access files.

Rename uploaded files. Do not preserve the original filename for storage. Generate a random UUID or hash-based filename. This prevents path traversal attacks where a filename like ../../../etc/passwd tricks your application into writing to an unintended location. Store the original filename in your database metadata if you need to display it to users.

Set Content-Disposition and Content-Type headers when serving. When users download uploaded files, set Content-Disposition: attachment to force download rather than inline rendering. Set Content-Type to the validated type, not the original header. For images displayed inline, set Content-Type to the specific image type and add X-Content-Type-Options: nosniff to prevent the browser from guessing the type.

Serving Uploaded Content Safely

Serving user-uploaded content from your application's domain is risky. If an uploaded HTML file or SVG is served from yourdomain.com, any JavaScript in that file executes in the context of your domain, with access to your cookies and your users' sessions.

Serve uploaded content from a separate domain. Use a dedicated domain — uploads.yourapp-cdn.com — that does not share cookies or origin with your main application. This isolates any malicious content from your application's security context. Ensure your Content Security Policy does not whitelist this uploads domain for script execution.

Process images server-side. For image uploads, re-encode the image on your server. Read the uploaded file, decode it using an image processing library like Sharp, and re-encode it as a new image. This strips any embedded metadata, scripts, or exploit payloads. The re-encoded image is a clean file that you generated, not an untrusted file from an external source.

Implement rate limiting on upload endpoints. File upload is resource-intensive — it consumes bandwidth, CPU for validation and processing, and storage space. Without rate limiting, an attacker can exhaust your resources by uploading thousands of files. Limit uploads per user per time window, and implement total storage quotas per user or organization.

Handling File Upload in Multi-Step Flows

Many applications need files uploaded as part of a larger workflow — a profile setup, a document submission, a support ticket. In these cases, upload the file first to a temporary staging area, validate it, and associate it with the entity only when the full form is submitted.

This pattern keeps your validation logic clean and prevents orphaned files when users abandon forms mid-completion. Run a background job that purges staged files older than a configurable threshold — twenty-four hours is typical.

For applications where uploaded files are shared between users — document collaboration, file sharing — add access control checks on every download request. A signed URL that expires in fifteen minutes is safer than a permanent public URL, especially for sensitive documents.

File upload is not a feature you add in an afternoon. It is a feature that requires careful validation, isolated storage, safe serving practices, and ongoing monitoring. Every shortcut you take becomes an attack vector. Build it right the first time, and it remains a reliable feature. Build it carelessly, and it becomes the vulnerability that makes the news.