EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
fileValidation.js
Go to the documentation of this file.
1/**
2 * @file File Upload Validation Middleware
3 * @module middleware/fileValidation
4 * @description
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.
8 *
9 * Security features:
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
15 *
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
21 * @example
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;
27 * });
28 */
29
30/**
31 * Validates file upload data for security and format requirements.
32 *
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)
41 * @example
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');
46 * } else {
47 * console.error('Error:', result.error);
48 * }
49 * @example
50 * // Invalid extension
51 * validateFileUpload('script.exe', 'TVqQAAMAAAA...')
52 * // => { isValid: false, error: "File type .exe not allowed..." }
53 * @example
54 * // Path traversal attempt
55 * validateFileUpload('../../etc/passwd', 'cm9vdDp4OjA6...')
56 * // => { isValid: false, error: "Invalid characters in filename" }
57 */
58function validateFileUpload(filename, data_base64, maxSizeMB = 10) {
59 // 1. Validate filename exists
60 if (!filename || typeof filename !== 'string') {
61 return {
62 isValid: false,
63 error: 'Filename is required'
64 };
65 }
66
67 // 2. Validate base64 data exists
68 if (!data_base64 || typeof data_base64 !== 'string') {
69 return {
70 isValid: false,
71 error: 'File data is required'
72 };
73 }
74
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
81 ];
82
83 const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
84
85 if (!ext || !allowedExtensions.includes(ext)) {
86 return {
87 isValid: false,
88 error: `File type ${ext || 'unknown'} not allowed. Allowed types: ${allowedExtensions.join(', ')}`
89 };
90 }
91
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];
97 }
98
99 if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanedBase64)) {
100 return {
101 isValid: false,
102 error: 'Invalid file data format'
103 };
104 }
105
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);
110
111 if (sizeInMB > maxSizeMB) {
112 return {
113 isValid: false,
114 error: `File size ${sizeInMB.toFixed(2)}MB exceeds limit of ${maxSizeMB}MB`
115 };
116 }
117
118 // 6. Prevent path traversal in filename
119 if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
120 return {
121 isValid: false,
122 error: 'Invalid characters in filename'
123 };
124 }
125
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
130
131 // 8. Ensure filename isn't empty after sanitization
132 if (!sanitizedFilename || sanitizedFilename === ext) {
133 return {
134 isValid: false,
135 error: 'Filename must contain valid characters'
136 };
137 }
138
139 return {
140 isValid: true,
141 sanitizedFilename,
142 cleanedBase64,
143 sizeInMB: sizeInMB.toFixed(2)
144 };
145}
146
147/**
148 * Express middleware for validating array of file attachments in request body.
149 *
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
163 * @example
164 * // Request body with attachments
165 * {
166 * "title": "Monthly Report",
167 * "attachments": [
168 * {
169 * "filename": "report.pdf",
170 * "data": "JVBERi0xLjQKJcfs..."
171 * }
172 * ]
173 * }
174 * @example
175 * // After middleware, req.body.attachments contains
176 * [
177 * {
178 * "filename": "report.pdf",
179 * "sanitizedFilename": "report.pdf",
180 * "data": "JVBERi0xLjQKJcfs...",
181 * "sizeInMB": "0.45"
182 * }
183 * ]
184 * @example
185 * // Error response
186 * {
187 * "error": "Invalid attachment: File type .exe not allowed..."
188 * }
189 */
190function validateAttachments(req, res, next) {
191 const { attachments } = req.body;
192
193 // If no attachments, skip validation
194 if (!attachments || !Array.isArray(attachments) || attachments.length === 0) {
195 return next();
196 }
197
198 const validatedAttachments = [];
199 const errors = [];
200
201 for (let i = 0; i < attachments.length; i++) {
202 const attachment = attachments[i];
203
204 if (!attachment.filename || !attachment.data) {
205 errors.push(`Attachment ${i + 1}: Missing filename or data`);
206 continue;
207 }
208
209 const result = validateFileUpload(attachment.filename, attachment.data);
210
211 if (!result.isValid) {
212 errors.push(`Attachment "${attachment.filename}": ${result.error}`);
213 } else {
214 validatedAttachments.push({
215 filename: result.sanitizedFilename,
216 data: result.cleanedBase64,
217 originalFilename: attachment.filename,
218 size: result.sizeInMB
219 });
220 }
221 }
222
223 if (errors.length > 0) {
224 return res.status(400).json({
225 error: 'File validation failed',
226 details: errors
227 });
228 }
229
230 // Replace original attachments with validated ones
231 req.body.validatedAttachments = validatedAttachments;
232 next();
233}
234
235module.exports = {
236 validateFileUpload,
237 validateAttachments
238};