2 * @file File Upload Validation Middleware
3 * @module middleware/fileValidation
5 * Provides comprehensive security validation for file uploads to prevent common
6 * vulnerabilities and abuse. Validates file format, size, content, and filename
7 * sanitization before processing uploads.
10 * - **Malicious file prevention**: Whitelist-based extension filtering
11 * - **DoS protection**: File size limits (default: 10MB)
12 * - **Path traversal prevention**: Filename sanitization
13 * - **Format validation**: Base64 format verification
14 * - **Auto-sanitization**: Removes dangerous characters from filenames
16 * Supported file types:
17 * - Documents: .pdf, .doc, .docx, .txt, .rtf
18 * - Spreadsheets: .xls, .xlsx, .csv
19 * - Images: .jpg, .jpeg, .png, .gif, .webp
20 * - Archives: .zip, .tar, .gz
22 * // Apply to document upload routes
23 * const { validateAttachments } = require('./middleware/fileValidation');
24 * router.post('/documents', authenticateToken, validateAttachments, async (req, res) => {
25 * // req.body.attachments validated and sanitized
26 * const files = req.body.attachments;
31 * Validates file upload data for security and format requirements.
33 * Performs comprehensive validation including filename sanitization, extension
34 * whitelisting, size checking, base64 format verification, and path traversal
35 * prevention. Returns validation result with sanitized filename on success.
36 * @function validateFileUpload
37 * @param {string} filename - Original filename from upload
38 * @param {string} data_base64 - Base64 encoded file data (with or without data URL prefix)
39 * @param {number} [maxSizeMB] - Maximum file size in megabytes (default: 10)
40 * @returns {object} Validation result with isValid, error (if invalid), or sanitizedFilename/cleanedBase64/sizeInMB (if valid)
42 * const result = validateFileUpload('report.pdf', 'JVBERi0xLjQK...', 5);
43 * if (result.isValid) {
44 * console.log('Sanitized:', result.sanitizedFilename);
45 * console.log('Size:', result.sizeInMB, 'MB');
47 * console.error('Error:', result.error);
50 * // Invalid extension
51 * validateFileUpload('script.exe', 'TVqQAAMAAAA...')
52 * // => { isValid: false, error: "File type .exe not allowed..." }
54 * // Path traversal attempt
55 * validateFileUpload('../../etc/passwd', 'cm9vdDp4OjA6...')
56 * // => { isValid: false, error: "Invalid characters in filename" }
58function validateFileUpload(filename, data_base64, maxSizeMB = 10) {
59 // 1. Validate filename exists
60 if (!filename || typeof filename !== 'string') {
63 error: 'Filename is required'
67 // 2. Validate base64 data exists
68 if (!data_base64 || typeof data_base64 !== 'string') {
71 error: 'File data is required'
75 // 3. Validate file extension
76 const allowedExtensions = [
77 '.pdf', '.doc', '.docx', '.txt', '.rtf', // Documents
78 '.xls', '.xlsx', '.csv', // Spreadsheets
79 '.jpg', '.jpeg', '.png', '.gif', '.webp', // Images
80 '.zip', '.tar', '.gz' // Archives
83 const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
85 if (!ext || !allowedExtensions.includes(ext)) {
88 error: `File type ${ext || 'unknown'} not allowed. Allowed types: ${allowedExtensions.join(', ')}`
92 // 4. Validate base64 format
93 // Remove data URL prefix if present (e.g., "data:image/png;base64,")
94 let cleanedBase64 = data_base64;
95 if (data_base64.includes(',')) {
96 cleanedBase64 = data_base64.split(',')[1];
99 if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanedBase64)) {
102 error: 'Invalid file data format'
106 // 5. Validate file size
107 // Base64 encoding increases size by ~33%, so decode to get actual size
108 const sizeInBytes = (cleanedBase64.length * 3) / 4;
109 const sizeInMB = sizeInBytes / (1024 * 1024);
111 if (sizeInMB > maxSizeMB) {
114 error: `File size ${sizeInMB.toFixed(2)}MB exceeds limit of ${maxSizeMB}MB`
118 // 6. Prevent path traversal in filename
119 if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
122 error: 'Invalid characters in filename'
126 // 7. Sanitize filename - remove dangerous characters
127 const sanitizedFilename = filename
128 .replace(/[^a-zA-Z0-9._-]/g, '_') // Replace special chars with underscore
129 .substring(0, 255); // Limit length
131 // 8. Ensure filename isn't empty after sanitization
132 if (!sanitizedFilename || sanitizedFilename === ext) {
135 error: 'Filename must contain valid characters'
143 sizeInMB: sizeInMB.toFixed(2)
148 * Express middleware for validating array of file attachments in request body.
150 * Validates all files in req.body.attachments array. Replaces attachments with
151 * validated objects containing sanitized filenames and cleaned base64 data. Returns
152 * 400 error if any attachment fails validation.
153 * @function validateAttachments
154 * @param {object} req - Express request object
155 * @param {object} req.body - Request body
156 * @param {object[]} [req.body.attachments] - Array of attachment objects
157 * @param {string} req.body.attachments[].filename - Original filename
158 * @param {string} req.body.attachments[].data - Base64 encoded file data
159 * @param {object} res - Express response object
160 * @param {Function} next - Express next middleware function
161 * @returns {void} Modifies req.body.attachments with validated data or sends 400 error
162 * @throws {400} Invalid attachment - Validation failed for one or more files
164 * // Request body with attachments
166 * "title": "Monthly Report",
169 * "filename": "report.pdf",
170 * "data": "JVBERi0xLjQKJcfs..."
175 * // After middleware, req.body.attachments contains
178 * "filename": "report.pdf",
179 * "sanitizedFilename": "report.pdf",
180 * "data": "JVBERi0xLjQKJcfs...",
187 * "error": "Invalid attachment: File type .exe not allowed..."
190function validateAttachments(req, res, next) {
191 const { attachments } = req.body;
193 // If no attachments, skip validation
194 if (!attachments || !Array.isArray(attachments) || attachments.length === 0) {
198 const validatedAttachments = [];
201 for (let i = 0; i < attachments.length; i++) {
202 const attachment = attachments[i];
204 if (!attachment.filename || !attachment.data) {
205 errors.push(`Attachment ${i + 1}: Missing filename or data`);
209 const result = validateFileUpload(attachment.filename, attachment.data);
211 if (!result.isValid) {
212 errors.push(`Attachment "${attachment.filename}": ${result.error}`);
214 validatedAttachments.push({
215 filename: result.sanitizedFilename,
216 data: result.cleanedBase64,
217 originalFilename: attachment.filename,
218 size: result.sizeInMB
223 if (errors.length > 0) {
224 return res.status(400).json({
225 error: 'File validation failed',
230 // Replace original attachments with validated ones
231 req.body.validatedAttachments = validatedAttachments;