File Upload Security: Preventing Malicious File Uploads and Vulnerabilities

File Upload Security: Preventing Malicious File Uploads and Vulnerabilities

Whitespots Team ·
file-upload
web
validation
malware

Introduction

File upload functionality is a common attack vector that can lead to remote code execution, stored XSS, malware distribution, and server compromise. Improper file upload handling has been exploited in countless breaches. This guide covers comprehensive file upload security with practical implementation examples.

Common File Upload Vulnerabilities

  1. Unrestricted file upload leading to RCE
  2. Insufficient file type validation
  3. Path traversal via filenames
  4. Malware upload and distribution
  5. Stored XSS via file content
  6. DoS via large files
  7. File overwrite vulnerabilities
  8. Information disclosure

Vulnerable File Upload Implementation

javascript
// VULNERABLE: Multiple security issues const express = require('express'); const multer = require('multer'); const path = require('path'); const upload = multer({ storage: multer.diskStorage({ destination: './uploads', filename: (req, file, cb) => { // Uses original filename - path traversal risk! cb(null, file.originalname); } }) }); app.post('/upload', upload.single('file'), (req, res) => { // No validation! // No file type checking! // No size limits! // Executes uploaded files if accessed! res.json({ filename: req.file.filename }); });

Secure File Upload Implementation

javascript
// SECURE: Comprehensive file upload security const express = require('express'); const multer = require('multer'); const path = require('path'); const crypto = require('crypto'); const { promisify } = require('util'); const fs = require('fs'); const fileType = require('file-type'); const sanitize = require('sanitize-filename'); const app = express(); // Configuration const UPLOAD_DIR = '/var/uploads'; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf' ]; // File filter const fileFilter = (req, file, cb) => { // Check MIME type if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) { return cb(new Error('Invalid file type'), false); } cb(null, true); }; // Storage configuration const storage = multer.diskStorage({ destination: (req, file, cb) => { // Use separate directory per user const userDir = path.join(UPLOAD_DIR, req.user.id); fs.mkdirSync(userDir, { recursive: true }); cb(null, userDir); }, filename: (req, file, cb) => { // Generate random filename const randomName = crypto.randomBytes(16).toString('hex'); const ext = path.extname(file.originalname).toLowerCase(); cb(null, `${randomName}${ext}`); } }); const upload = multer({ storage, fileFilter, limits: { fileSize: MAX_FILE_SIZE, files: 1 } }); // Upload endpoint app.post('/upload', upload.single('file'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } // Verify file type by content (magic bytes) const buffer = await fs.promises.readFile(req.file.path); const type = await fileType.fromBuffer(buffer); if (!type || !ALLOWED_MIME_TYPES.includes(type.mime)) { await fs.promises.unlink(req.file.path); return res.status(400).json({ error: 'Invalid file content' }); } // Scan for malware (example with ClamAV) const isSafe = await scanFile(req.file.path); if (!isSafe) { await fs.promises.unlink(req.file.path); return res.status(400).json({ error: 'Malware detected' }); } // Store metadata in database const fileRecord = await db.files.create({ userId: req.user.id, originalName: sanitize(req.file.originalname), storedName: req.file.filename, mimeType: type.mime, size: req.file.size, uploadedAt: new Date() }); res.json({ id: fileRecord.id, filename: fileRecord.originalName, size: fileRecord.size }); } catch (error) { // Clean up file on error if (req.file) { await fs.promises.unlink(req.file.path).catch(() => {}); } res.status(500).json({ error: 'Upload failed' }); } });

File Type Validation

javascript
// Multi-layer file type validation const fileType = require('file-type'); const mime = require('mime-types'); async function validateFileType(filePath, originalName, declaredMimeType) { // 1. Check file extension const ext = path.extname(originalName).toLowerCase(); const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf']; if (!allowedExtensions.includes(ext)) { return { valid: false, reason: 'Invalid extension' }; } // 2. Check declared MIME type const expectedMime = mime.lookup(ext); if (declaredMimeType !== expectedMime) { return { valid: false, reason: 'MIME type mismatch' }; } // 3. Check actual file content (magic bytes) const buffer = await fs.promises.readFile(filePath); const actualType = await fileType.fromBuffer(buffer); if (!actualType) { return { valid: false, reason: 'Could not determine file type' }; } if (actualType.mime !== declaredMimeType) { return { valid: false, reason: 'Content does not match declared type' }; } // 4. For images, validate with sharp if (actualType.mime.startsWith('image/')) { try { const sharp = require('sharp'); await sharp(buffer).metadata(); } catch (error) { return { valid: false, reason: 'Invalid image file' }; } } return { valid: true }; }

Malware Scanning

javascript
// ClamAV integration for malware scanning const NodeClam = require('clamscan'); let clamscan; async function initClamAV() { clamscan = await new NodeClam().init({ removeInfected: false, quarantineInfected: false, scanLog: '/var/log/clamav-scan.log', debugMode: false, clamdscan: { socket: '/var/run/clamav/clamd.ctl', timeout: 60000, localFallback: true } }); } async function scanFile(filePath) { try { const { isInfected, viruses } = await clamscan.isInfected(filePath); if (isInfected) { console.log(`Malware detected in ${filePath}:`, viruses); return false; } return true; } catch (error) { console.error('Virus scan error:', error); // Fail secure - reject file if scan fails return false; } } // Use in upload handler app.post('/upload', upload.single('file'), async (req, res) => { const isSafe = await scanFile(req.file.path); if (!isSafe) { await fs.promises.unlink(req.file.path); return res.status(400).json({ error: 'File failed security scan' }); } // Continue processing... });

Preventing Path Traversal

javascript
// Secure filename handling const crypto = require('crypto'); const sanitize = require('sanitize-filename'); function generateSafeFilename(originalName) { // Remove path components const basename = path.basename(originalName); // Sanitize filename const safe = sanitize(basename); // Generate unique name const hash = crypto.randomBytes(8).toString('hex'); const ext = path.extname(safe).toLowerCase(); const name = path.basename(safe, ext); return `${name}-${hash}${ext}`; } // Validate and sanitize paths function validateFilePath(userProvidedPath, baseDir) { // Resolve to absolute path const resolvedPath = path.resolve(baseDir, userProvidedPath); // Ensure it's within base directory if (!resolvedPath.startsWith(path.resolve(baseDir))) { throw new Error('Path traversal detected'); } return resolvedPath; }

Secure File Serving

javascript
// Secure file download app.get('/download/:fileId', async (req, res) => { try { // Get file metadata from database const file = await db.files.findOne({ where: { id: req.params.fileId, userId: req.user.id // Authorization check } }); if (!file) { return res.status(404).json({ error: 'File not found' }); } const filePath = path.join(UPLOAD_DIR, req.user.id, file.storedName); // Verify file exists if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'File not found' }); } // Set secure headers res.setHeader('Content-Type', file.mimeType); res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.originalName)}"`); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Content-Security-Policy', "default-src 'none'"); // Stream file const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); } catch (error) { res.status(500).json({ error: 'Download failed' }); } });

Image Processing for Security

javascript
// Sanitize images by reprocessing const sharp = require('sharp'); async function sanitizeImage(inputPath, outputPath) { try { await sharp(inputPath) .resize(2048, 2048, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 85 }) .toFile(outputPath); // Remove original await fs.promises.unlink(inputPath); return true; } catch (error) { console.error('Image processing failed:', error); return false; } } // Use after upload app.post('/upload/image', upload.single('image'), async (req, res) => { const tempPath = req.file.path; const finalPath = tempPath + '.sanitized.jpg'; const success = await sanitizeImage(tempPath, finalPath); if (!success) { return res.status(400).json({ error: 'Image processing failed' }); } // Continue with sanitized image... });

Cloud Storage Security

javascript
// AWS S3 secure upload const AWS = require('aws-sdk'); const s3 = new AWS.S3(); async function uploadToS3(file, userId) { const key = `${userId}/${crypto.randomUUID()}${path.extname(file.originalname)}`; const params = { Bucket: process.env.S3_BUCKET, Key: key, Body: fs.createReadStream(file.path), ContentType: file.mimetype, ServerSideEncryption: 'AES256', Metadata: { 'original-name': file.originalname, 'user-id': userId }, // ACL: 'private', // Default StorageClass: 'STANDARD_IA' }; try { await s3.upload(params).promise(); // Delete local file await fs.promises.unlink(file.path); return { key, url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}` }; } catch (error) { throw new Error(`S3 upload failed: ${error.message}`); } } // Generate pre-signed URL for download async function getDownloadUrl(key, userId) { // Verify ownership const file = await db.files.findOne({ where: { s3Key: key, userId } }); if (!file) { throw new Error('Unauthorized'); } const url = s3.getSignedUrl('getObject', { Bucket: process.env.S3_BUCKET, Key: key, Expires: 3600, // 1 hour ResponseContentDisposition: `attachment; filename="${file.originalName}"` }); return url; }

File Upload Rate Limiting

javascript
// Rate limit uploads per user const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const uploadLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: 'upload:limit:' }), windowMs: 60 * 60 * 1000, // 1 hour max: 10, // 10 uploads per hour keyGenerator: (req) => req.user.id, handler: (req, res) => { res.status(429).json({ error: 'Upload limit exceeded', retryAfter: req.rateLimit.resetTime }); } }); app.post('/upload', uploadLimiter, upload.single('file'), uploadHandler);

File Upload Security Checklist

  • ✅ Validate file type by extension, MIME, and content
  • ✅ Generate random filenames
  • ✅ Store files outside web root
  • ✅ Scan for malware
  • ✅ Set file size limits
  • ✅ Implement rate limiting
  • ✅ Sanitize filenames
  • ✅ Prevent path traversal
  • ✅ Use proper file permissions
  • ✅ Reprocess images
  • ✅ Set secure HTTP headers for downloads
  • ✅ Implement access controls
  • ✅ Log upload activity
  • ✅ Use virus scanning
  • ✅ Encrypt files at rest
  • ✅ Validate on client and server

Conclusion

File upload security requires multiple layers of defense including validation, scanning, sanitization, and secure storage. By implementing comprehensive checks on file type, content, and size, along with malware scanning and proper access controls, you prevent file upload vulnerabilities from compromising your application.

File upload security should be regularly tested and reviewed as part of your security assessment process. For comprehensive file upload security reviews and implementation guidance, contact the Whitespots team for expert consultation.