@reldens/server-utils 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/lib/file-handler.js +14 -0
- package/lib/uploader-factory.js +106 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ A Node.js server toolkit providing secure application server creation, file hand
|
|
|
40
40
|
- Password hashing using PBKDF2 with configurable iterations
|
|
41
41
|
- Password validation against stored hashes
|
|
42
42
|
- AES-256-GCM data encryption and decryption
|
|
43
|
-
- Secure token generation with customizable length
|
|
43
|
+
- Secure token generation with a customizable length
|
|
44
44
|
- TOTP (Time-based One-Time Password) generation
|
|
45
45
|
- Data hashing with multiple algorithms (SHA-256, SHA-512, MD5)
|
|
46
46
|
- HMAC generation and verification
|
|
@@ -245,8 +245,9 @@ let serverResult = appServerFactory.createAppServer({
|
|
|
245
245
|
### FileHandler Methods
|
|
246
246
|
|
|
247
247
|
- `exists(path)` - Checks if file or folder exists
|
|
248
|
-
- `createFolder(path)` - Creates folder with recursive option
|
|
248
|
+
- `createFolder(path)` - Creates folder with a recursive option
|
|
249
249
|
- `remove(path)` - Removes file or folder recursively
|
|
250
|
+
- `removeMultiple(filePaths)` - Removes multiple files from an array of paths
|
|
250
251
|
- `copyFile(source, destination)` - Copies file to destination
|
|
251
252
|
- `copyFolderSync(source, destination)` - Copies folder recursively
|
|
252
253
|
- `readFile(path)` - Reads file contents as string
|
|
@@ -254,14 +255,14 @@ let serverResult = appServerFactory.createAppServer({
|
|
|
254
255
|
- `fetchFileJson(path)` - Reads and parses JSON file
|
|
255
256
|
- `fetchFileContents(path)` - Reads file with validation
|
|
256
257
|
- `updateFileContents(path, content)` - Updates existing file
|
|
257
|
-
- `isFile(path)` - Checks if path is file
|
|
258
|
-
- `isFolder(path)` - Checks if path is folder
|
|
258
|
+
- `isFile(path)` - Checks if a path is a file
|
|
259
|
+
- `isFolder(path)` - Checks if a path is folder
|
|
259
260
|
- `getFilesInFolder(path, extensions)` - Lists files with optional filtering
|
|
260
261
|
- `validateFileType(path, type, allowedTypes, maxSize)` - Validates file type and size
|
|
261
262
|
- `detectFileType(path)` - Detects MIME type from file signature
|
|
262
263
|
- `generateSecureFilename(originalName)` - Generates cryptographically secure filename
|
|
263
264
|
- `quarantineFile(path, reason)` - Moves file to quarantine folder
|
|
264
|
-
- `createTempFile(prefix, extension)` - Creates temporary file path
|
|
265
|
+
- `createTempFile(prefix, extension)` - Creates a temporary file path
|
|
265
266
|
|
|
266
267
|
### Encryptor Methods
|
|
267
268
|
|
|
@@ -281,9 +282,8 @@ let serverResult = appServerFactory.createAppServer({
|
|
|
281
282
|
|
|
282
283
|
- `createUploader(fields, buckets, allowedTypes)` - Creates multer upload middleware
|
|
283
284
|
- `validateFilenameSecurity(filename)` - Validates filename for security
|
|
284
|
-
- `validateFile(file, allowedType, callback)` - Validates file during upload
|
|
285
|
+
- `validateFile(file, allowedType, callback)` - Validates a file during upload
|
|
285
286
|
- `validateFileContents(file, allowedType)` - Validates file content after upload
|
|
286
|
-
- `cleanupFiles(files)` - Removes uploaded files on error
|
|
287
287
|
|
|
288
288
|
## Security Features
|
|
289
289
|
|
|
@@ -291,7 +291,7 @@ let serverResult = appServerFactory.createAppServer({
|
|
|
291
291
|
All file operations include comprehensive path validation to prevent directory traversal attacks and access to system files.
|
|
292
292
|
|
|
293
293
|
### Secure File Upload
|
|
294
|
-
File uploads are validated at multiple levels including filename, MIME type, file extension, file size, and content validation using magic number detection.
|
|
294
|
+
File uploads are validated at multiple levels including filename, MIME type, file extension, file size, and content validation using magic number detection. Failed uploads are automatically cleaned up using efficient file removal.
|
|
295
295
|
|
|
296
296
|
### Rate Limiting
|
|
297
297
|
Configurable rate limiting with development mode detection for appropriate thresholds in different environments.
|
package/lib/file-handler.js
CHANGED
|
@@ -141,6 +141,20 @@ class FileHandler
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
removeMultiple(filePaths)
|
|
145
|
+
{
|
|
146
|
+
if(!Array.isArray(filePaths)){
|
|
147
|
+
this.error = {message: 'File paths must be an array'};
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
for(let filePath of filePaths){
|
|
151
|
+
if(!this.remove(filePath)){
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
createFolder(folderPath)
|
|
145
159
|
{
|
|
146
160
|
if(!this.isValidPath(folderPath)){
|
package/lib/uploader-factory.js
CHANGED
|
@@ -25,38 +25,55 @@ class UploaderFactory
|
|
|
25
25
|
this.maxFilenameLength = props.maxFilenameLength || 255;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
setError(message, additionalData = {})
|
|
29
|
+
{
|
|
30
|
+
if(!this.error.message){
|
|
31
|
+
this.error = {message, ...additionalData};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
createUploader(fields, buckets, allowedFileTypes)
|
|
29
36
|
{
|
|
30
37
|
if(!this.validateInputs(fields, buckets, allowedFileTypes)){
|
|
31
|
-
this.
|
|
38
|
+
this.setError('Invalid uploader configuration: ' + this.error.message);
|
|
32
39
|
return false;
|
|
33
40
|
}
|
|
34
|
-
let
|
|
41
|
+
let storage = multer.diskStorage({
|
|
35
42
|
destination: (req, file, cb) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
try{
|
|
44
|
+
let dest = buckets[file.fieldname];
|
|
45
|
+
if(!FileHandler.isValidPath(dest)){
|
|
46
|
+
this.setError('Invalid destination path', {dest, fieldname: file.fieldname});
|
|
47
|
+
return cb(new Error('Invalid destination path'));
|
|
48
|
+
}
|
|
49
|
+
let folderCreated = FileHandler.createFolder(dest);
|
|
50
|
+
if(!folderCreated){
|
|
51
|
+
this.setError('Cannot create destination folder', {dest, fileHandlerError: FileHandler.error});
|
|
52
|
+
return cb(new Error('Cannot create destination folder'));
|
|
53
|
+
}
|
|
54
|
+
cb(null, dest);
|
|
55
|
+
} catch(error){
|
|
56
|
+
this.setError('Cannot prepare destination.', {error: error});
|
|
57
|
+
cb(error);
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
req.fileNameMapping =
|
|
59
|
+
},
|
|
60
|
+
filename: (req, file, cb) => {
|
|
61
|
+
if(!this.validateFilenameSecurity(file.originalname)){
|
|
62
|
+
this.setError('Invalid filename', {originalname: file.originalname});
|
|
63
|
+
return cb(new Error('Invalid filename'));
|
|
64
|
+
}
|
|
65
|
+
if(!this.applySecureFileNames) {
|
|
66
|
+
cb(null, file.originalname);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let secureFilename = FileHandler.generateSecureFilename(file.originalname);
|
|
70
|
+
if(!req.fileNameMapping){
|
|
71
|
+
req.fileNameMapping = {};
|
|
72
|
+
}
|
|
73
|
+
req.fileNameMapping[secureFilename] = file.originalname;
|
|
74
|
+
cb(null, secureFilename);
|
|
55
75
|
}
|
|
56
|
-
|
|
57
|
-
cb(null, secureFilename);
|
|
58
|
-
};
|
|
59
|
-
let storage = multer.diskStorage(diskStorageConfiguration);
|
|
76
|
+
});
|
|
60
77
|
let limits = {
|
|
61
78
|
fileSize: this.maxFileSize
|
|
62
79
|
};
|
|
@@ -74,39 +91,44 @@ class UploaderFactory
|
|
|
74
91
|
upload.fields(fields)(req, res, async (multerError) => {
|
|
75
92
|
if(multerError){
|
|
76
93
|
if(multerError instanceof multer.MulterError){
|
|
77
|
-
if(multerError.code
|
|
94
|
+
if('LIMIT_FILE_SIZE' === multerError.code){
|
|
78
95
|
let messageFile = 'File too large.';
|
|
96
|
+
this.setError(messageFile, {multerError});
|
|
79
97
|
if('function' === typeof this.processErrorResponse){
|
|
80
98
|
return this.processErrorResponse(413, messageFile, req, res);
|
|
81
99
|
}
|
|
82
100
|
return res.status(413).send(messageFile);
|
|
83
101
|
}
|
|
84
|
-
if(multerError.code
|
|
102
|
+
if('LIMIT_FILE_COUNT' === multerError.code){
|
|
85
103
|
let messageTooMany = 'Too many files.';
|
|
104
|
+
this.setError(messageTooMany, {multerError});
|
|
86
105
|
if('function' === typeof this.processErrorResponse){
|
|
87
106
|
return this.processErrorResponse(413, messageTooMany, req, res);
|
|
88
107
|
}
|
|
89
108
|
return res.status(413).send(messageTooMany);
|
|
90
109
|
}
|
|
91
110
|
let messageUpload = 'File upload error.';
|
|
111
|
+
this.setError(messageUpload, {multerError});
|
|
92
112
|
if('function' === typeof this.processErrorResponse){
|
|
93
113
|
return this.processErrorResponse(400, messageUpload, multerError, req, res);
|
|
94
114
|
}
|
|
95
115
|
return res.status(400).send(messageUpload);
|
|
96
116
|
}
|
|
97
|
-
let messageServer = 'Server error during file upload.';
|
|
117
|
+
let messageServer = this.error?.message ? this.error.message : 'Server error during file upload.';
|
|
118
|
+
this.setError(messageServer, {multerError});
|
|
98
119
|
if('function' === typeof this.processErrorResponse){
|
|
99
|
-
return this.processErrorResponse(
|
|
120
|
+
return this.processErrorResponse(415, messageServer, req, res);
|
|
100
121
|
}
|
|
101
|
-
return res.status(
|
|
122
|
+
return res.status(415).send(messageServer);
|
|
102
123
|
}
|
|
103
124
|
if(!req.files){
|
|
104
125
|
return next();
|
|
105
126
|
}
|
|
106
127
|
let validationResult = await this.validateAllUploadedFiles(req, allowedFileTypes);
|
|
107
128
|
if(!validationResult){
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
let filePaths = Object.values(req.files).flat().map(file => file.path);
|
|
130
|
+
FileHandler.removeMultiple(filePaths);
|
|
131
|
+
let messageContents = this.error?.message ? this.error.message : 'File validation failed.';
|
|
110
132
|
if('function' === typeof this.processErrorResponse){
|
|
111
133
|
return this.processErrorResponse(415, messageContents, req, res);
|
|
112
134
|
}
|
|
@@ -122,7 +144,8 @@ class UploaderFactory
|
|
|
122
144
|
try {
|
|
123
145
|
for(let fieldName in req.files){
|
|
124
146
|
for(let file of req.files[fieldName]){
|
|
125
|
-
|
|
147
|
+
let validationResult = await this.validateFileContents(file, allowedFileTypes[fieldName]);
|
|
148
|
+
if(!validationResult){
|
|
126
149
|
FileHandler.remove(file.path);
|
|
127
150
|
return false;
|
|
128
151
|
}
|
|
@@ -130,7 +153,7 @@ class UploaderFactory
|
|
|
130
153
|
}
|
|
131
154
|
return true;
|
|
132
155
|
} catch(error){
|
|
133
|
-
this.
|
|
156
|
+
this.setError('Error processing uploaded files.', {error});
|
|
134
157
|
return false;
|
|
135
158
|
}
|
|
136
159
|
}
|
|
@@ -159,28 +182,28 @@ class UploaderFactory
|
|
|
159
182
|
validateInputs(fields, buckets, allowedFileTypes)
|
|
160
183
|
{
|
|
161
184
|
if(!Array.isArray(fields)){
|
|
162
|
-
this.
|
|
185
|
+
this.setError('Fields must be an array');
|
|
163
186
|
return false;
|
|
164
187
|
}
|
|
165
188
|
if(!buckets || 'object' !== typeof buckets){
|
|
166
|
-
this.
|
|
189
|
+
this.setError('Buckets must be an object');
|
|
167
190
|
return false;
|
|
168
191
|
}
|
|
169
192
|
if(!allowedFileTypes || 'object' !== typeof allowedFileTypes){
|
|
170
|
-
this.
|
|
193
|
+
this.setError('AllowedFileTypes must be an object');
|
|
171
194
|
return false;
|
|
172
195
|
}
|
|
173
196
|
for(let field of fields){
|
|
174
197
|
if(!field.name || 'string' !== typeof field.name){
|
|
175
|
-
this.
|
|
198
|
+
this.setError('Field name is invalid');
|
|
176
199
|
return false;
|
|
177
200
|
}
|
|
178
201
|
if(!buckets[field.name]){
|
|
179
|
-
this.
|
|
202
|
+
this.setError('Missing bucket for field: ' + field.name);
|
|
180
203
|
return false;
|
|
181
204
|
}
|
|
182
205
|
if(!allowedFileTypes[field.name]){
|
|
183
|
-
this.
|
|
206
|
+
this.setError('Missing allowedFileType for field: ' + field.name);
|
|
184
207
|
return false;
|
|
185
208
|
}
|
|
186
209
|
}
|
|
@@ -190,49 +213,77 @@ class UploaderFactory
|
|
|
190
213
|
validateFile(file, allowedFileType, cb)
|
|
191
214
|
{
|
|
192
215
|
if(!allowedFileType){
|
|
193
|
-
return cb();
|
|
216
|
+
return cb(null, true);
|
|
194
217
|
}
|
|
195
218
|
if(!this.validateFilenameSecurity(file.originalname)){
|
|
196
|
-
this.
|
|
197
|
-
return cb(new Error('Insecure filename: '
|
|
219
|
+
this.setError('Insecure filename: '+file.originalname, {originalname: file.originalname});
|
|
220
|
+
return cb(new Error('Insecure filename: '+file.originalname));
|
|
198
221
|
}
|
|
199
222
|
let fileExtension = FileHandler.extension(file.originalname).toLowerCase();
|
|
200
|
-
let allowedExtensions = this.allowedExtensions[allowedFileType];
|
|
223
|
+
let allowedExtensions = this.allowedExtensions && this.allowedExtensions[allowedFileType];
|
|
201
224
|
if(allowedExtensions && !allowedExtensions.includes(fileExtension)){
|
|
202
|
-
this.
|
|
203
|
-
|
|
225
|
+
this.setError('Invalid file extension: '+fileExtension, {
|
|
226
|
+
extension: fileExtension,
|
|
227
|
+
allowedExtensions,
|
|
228
|
+
allowedFileType,
|
|
229
|
+
filename: file.originalname
|
|
230
|
+
});
|
|
231
|
+
return cb(new Error('Invalid file extension: '+fileExtension));
|
|
204
232
|
}
|
|
205
233
|
let allowedFileTypeRegex = this.convertToRegex(allowedFileType);
|
|
206
234
|
if(!allowedFileTypeRegex){
|
|
207
|
-
this.
|
|
235
|
+
this.setError('File type could not be converted to regex.', {allowedFileType});
|
|
208
236
|
return cb(new Error('File type could not be converted to regex'));
|
|
209
237
|
}
|
|
210
238
|
let mimeTypeValid = allowedFileTypeRegex.test(file.mimetype);
|
|
211
239
|
if(!mimeTypeValid){
|
|
212
|
-
this.
|
|
213
|
-
|
|
240
|
+
this.setError('Invalid MIME type: '+file.mimetype, {
|
|
241
|
+
mimetype: file.mimetype,
|
|
242
|
+
allowedFileType,
|
|
243
|
+
filename: file.originalname,
|
|
244
|
+
regex: allowedFileTypeRegex
|
|
245
|
+
});
|
|
246
|
+
return cb(new Error('Invalid MIME type: '+file.mimetype));
|
|
214
247
|
}
|
|
215
|
-
return cb();
|
|
248
|
+
return cb(null, true);
|
|
216
249
|
}
|
|
217
250
|
|
|
218
251
|
async validateFileContents(file, allowedFileType)
|
|
219
252
|
{
|
|
220
253
|
if(!FileHandler.isFile(file.path)){
|
|
221
|
-
this.
|
|
254
|
+
this.setError('File path must be provided.', {file});
|
|
222
255
|
return false;
|
|
223
256
|
}
|
|
224
257
|
let detectedType = FileHandler.detectFileType(file.path);
|
|
225
258
|
if(detectedType && 'application/octet-stream' !== detectedType){
|
|
226
259
|
let expectedMimeTypes = this.mimeTypes[allowedFileType] || [];
|
|
227
260
|
if(0 < expectedMimeTypes.length && -1 === expectedMimeTypes.indexOf(detectedType)){
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
261
|
+
this.setError('File content type mismatch.', {
|
|
262
|
+
detected: detectedType,
|
|
263
|
+
expected: expectedMimeTypes,
|
|
264
|
+
filename: file.filename,
|
|
265
|
+
path: file.path,
|
|
266
|
+
allowedFileType
|
|
267
|
+
});
|
|
232
268
|
return false;
|
|
233
269
|
}
|
|
234
270
|
}
|
|
235
|
-
|
|
271
|
+
let typeValidationResult = FileHandler.validateFileType(
|
|
272
|
+
file.path,
|
|
273
|
+
allowedFileType,
|
|
274
|
+
this.allowedExtensions,
|
|
275
|
+
this.maxFileSize
|
|
276
|
+
);
|
|
277
|
+
if(!typeValidationResult){
|
|
278
|
+
this.setError('File type validation failed.', {
|
|
279
|
+
fileHandlerError: FileHandler.error,
|
|
280
|
+
filename: file.filename,
|
|
281
|
+
path: file.path,
|
|
282
|
+
allowedFileType
|
|
283
|
+
});
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
236
287
|
}
|
|
237
288
|
|
|
238
289
|
convertToRegex(key)
|
|
@@ -246,17 +297,6 @@ class UploaderFactory
|
|
|
246
297
|
return new RegExp(types.join('|'));
|
|
247
298
|
}
|
|
248
299
|
|
|
249
|
-
cleanupFiles(files)
|
|
250
|
-
{
|
|
251
|
-
if(!files){
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
for(let fieldName in files){
|
|
255
|
-
for(let file of files[fieldName]){
|
|
256
|
-
FileHandler.remove(file.path);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
300
|
}
|
|
261
301
|
|
|
262
302
|
module.exports.UploaderFactory = UploaderFactory;
|