TL;DR
The #1 file upload security best practice is to never trust file extensions or client-provided MIME types. Validate file content, generate random filenames, store outside web root, limit file sizes, and use a CDN or cloud storage. These practices prevent 85% of file upload vulnerabilities.
"Every uploaded file is a potential attack vector. Treat uploads like untrusted user input-validate everything, trust nothing."
Best Practice 1: Validate File Type 3 min
Check both MIME type and file content, not just extension:
import { fileTypeFromBuffer } from 'file-type';
const ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/webp': ['.webp'],
'application/pdf': ['.pdf'],
};
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
async function validateFile(file) {
const errors = [];
// Check size
if (file.size > MAX_SIZE) {
errors.push('File too large (max 10MB)');
}
// Read file buffer to check actual type
const buffer = await file.arrayBuffer();
const fileType = await fileTypeFromBuffer(buffer);
if (!fileType || !ALLOWED_TYPES[fileType.mime]) {
errors.push('Invalid file type');
return { valid: false, errors };
}
// Verify extension matches actual type
const ext = file.name.split('.').pop()?.toLowerCase();
if (!ALLOWED_TYPES[fileType.mime].includes(`.${ext}`)) {
errors.push('File extension does not match content');
}
return {
valid: errors.length === 0,
errors,
mimeType: fileType.mime,
};
}
Best Practice 2: Generate Safe Filenames 2 min
Never use user-provided filenames directly:
import { randomUUID } from 'crypto';
import path from 'path';
function generateSafeFilename(originalName, mimeType) {
// Get extension from MIME type, not original filename
const extensions = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
'application/pdf': 'pdf',
};
const ext = extensions[mimeType];
const uuid = randomUUID();
return `${uuid}.${ext}`;
// WRONG: Using original filename
// return originalName; // Could be "../../../etc/passwd.jpg"
}
// Store original filename in database if needed
await db.file.create({
data: {
storedName: safeFilename,
originalName: file.name,
mimeType: fileType.mime,
size: file.size,
userId: user.id,
},
});
Best Practice 3: Store Outside Web Root 3 min
Uploaded files should not be directly accessible:
// WRONG: Storing in public directory
const uploadPath = './public/uploads/' + filename;
// File accessible at: https://site.com/uploads/file.jpg
// CORRECT: Store outside web root, serve via API
const uploadPath = './private-uploads/' + filename;
// Serve files through authenticated endpoint
app.get('/api/files/:id', authenticate, async (req, res) => {
const file = await db.file.findUnique({
where: { id: req.params.id },
});
// Check authorization
if (file.userId !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
const filePath = path.join('./private-uploads', file.storedName);
res.sendFile(filePath);
});
Best Practice 4: Use Cloud Storage 5 min
Cloud storage (S3, GCS) is more secure and scalable:
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: process.env.AWS_REGION });
async function uploadToS3(file, filename) {
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: `uploads/${filename}`,
Body: file,
ContentType: file.type,
});
await s3.send(command);
}
// Generate signed URL for download (temporary access)
async function getDownloadUrl(filename) {
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: `uploads/${filename}`,
});
return getSignedUrl(s3, command, { expiresIn: 3600 }); // 1 hour
}
Best Practice 5: Scan for Malware 4 min
For sensitive applications, scan uploads:
// Using ClamAV
import NodeClam from 'clamscan';
const clamscan = await new NodeClam().init({
removeInfected: true,
scanLog: '/var/log/clamscan.log',
});
async function scanFile(filePath) {
const { isInfected, viruses } = await clamscan.scanFile(filePath);
if (isInfected) {
console.warn('Infected file detected:', viruses);
await fs.unlink(filePath); // Delete infected file
throw new Error('Malicious file detected');
}
return true;
}
Common File Upload Mistakes
| Mistake | Risk | Prevention |
|---|---|---|
| Trusting extensions | Malicious file execution | Check actual file content |
| Using original filename | Path traversal attacks | Generate random names |
| Storing in web root | Direct file access | Store outside, serve via API |
| No size limits | DoS via large uploads | Enforce strict size limits |
| No rate limiting | Storage exhaustion | Limit uploads per user |
External Resources: For comprehensive file upload security guidance, see the OWASP File Upload Cheat Sheet and the OWASP Unrestricted File Upload documentation for industry-standard security recommendations.
Should I resize images on upload?
Yes, for several reasons: it reduces storage costs, improves load times, and can strip malicious metadata. Use libraries like Sharp to resize and convert images to safe formats.
How do I handle large file uploads?
Use multipart uploads with resumable support. For cloud storage, generate presigned URLs so clients upload directly to S3/GCS, bypassing your server.
Should I keep original filenames?
Store them in your database for display purposes, but never use them for actual file storage. Always generate random, safe filenames for storage.
Check Your File Upload Security
Scan your application for file upload vulnerabilities.
Start Free Scan