How to Secure File Uploads
File uploads are one of the riskiest features in web applications. Without proper validation, attackers can upload malicious scripts, oversized files, or content that exploits other users. This guide covers every aspect of secure file handling.
Find security issues automatically before attackers do.
Follow These Steps
Validate file types by content, not just extension
File extensions can be spoofed. Check the actual file content using magic bytes.
// Check file magic bytes (Node.js)
import { fileTypeFromBuffer } from 'file-type'
async function validateFileType(buffer: Buffer, allowedTypes: string[]) {
const type = await fileTypeFromBuffer(buffer)
if (!type || !allowedTypes.includes(type.mime)) {
throw new Error('Invalid file type')
}
return type
}
// Usage
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']
await validateFileType(fileBuffer, allowed)Enforce file size limits
Set maximum file sizes to prevent resource exhaustion.
// Express.js with multer
import multer from 'multer'
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5MB max
files: 1 // 1 file per request
}
})
// Next.js route config
export const config = {
api: { bodyParser: { sizeLimit: '5mb' } }
}Rename uploaded files
Never use the original filename. Generate a random name to prevent path traversal and overwrite attacks.
import { randomUUID } from 'crypto'
import path from 'path'
function generateSafeFilename(originalName: string): string {
const ext = path.extname(originalName).toLowerCase()
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.pdf']
if (!allowedExtensions.includes(ext)) throw new Error('Invalid extension')
return `${randomUUID()}${ext}`
}Store files outside the web root
Use cloud storage (S3, Supabase Storage) or a directory not served by your web server.
// Upload to Supabase Storage
const { data, error } = await supabase.storage
.from('uploads')
.upload(`user-files/${userId}/${safeFilename}`, fileBuffer, {
contentType: validatedType.mime,
upsert: false
})
// Serve via signed URLs (time-limited access)
const { data: urlData } = await supabase.storage
.from('uploads')
.createSignedUrl(`user-files/${userId}/${safeFilename}`, 3600)Set correct Content-Type and Content-Disposition headers when serving
Prevent browsers from executing uploaded files as code.
// When serving uploaded files
res.setHeader('Content-Type', file.mimeType)
res.setHeader('Content-Disposition', 'attachment; filename="file.pdf"')
res.setHeader('X-Content-Type-Options', 'nosniff')What You'll Achieve
File uploads are validated by content type, limited in size, stored with random filenames in cloud storage, and served with secure headers. Your application is protected against malicious file upload attacks.
Common Mistakes to Avoid
Mistake
Validating only the file extension
Fix
File extensions can be spoofed. Use magic byte detection (file-type library) to verify actual content.
Mistake
Storing uploads in the public web directory
Fix
Uploaded files in the web root can be executed by the server. Use cloud storage or a directory outside the web root.
Mistake
Using the original filename
Fix
Original filenames can contain path traversal sequences (../../). Always generate a random filename.
Frequently Asked Questions
Is checking the file extension enough?
No. Extensions can be changed trivially. Use magic byte detection to verify the actual file type matches the expected type.
Where should I store uploaded files?
Use cloud storage like S3, Supabase Storage, or Cloudflare R2. Never store in your web root. Serve through signed URLs with proper Content-Type headers.
Ready to Secure Your App?
VAS automatically scans your deployed app for the security issues covered in this guide. Get actionable results in minutes.
Start Security Scan