@reldens/server-utils 0.16.0 → 0.18.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/lib/app-server-factory.js +236 -44
- package/lib/encryptor.js +125 -10
- package/lib/file-handler.js +152 -41
- package/lib/uploader-factory.js +86 -50
- package/package.json +3 -3
|
@@ -51,6 +51,23 @@ class AppServerFactory
|
|
|
51
51
|
this.tooManyRequestsMessage = 'Too many requests, please try again later.';
|
|
52
52
|
this.error = {};
|
|
53
53
|
this.processErrorResponse = false;
|
|
54
|
+
this.port = 3000;
|
|
55
|
+
this.autoListen = false;
|
|
56
|
+
this.domains = [];
|
|
57
|
+
this.useVirtualHosts = false;
|
|
58
|
+
this.defaultDomain = '';
|
|
59
|
+
this.maxRequestSize = '10mb';
|
|
60
|
+
this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
|
|
61
|
+
this.staticOptions = {
|
|
62
|
+
maxAge: '1d',
|
|
63
|
+
etag: true,
|
|
64
|
+
lastModified: true,
|
|
65
|
+
index: false,
|
|
66
|
+
setHeaders: function(res){
|
|
67
|
+
res.set('X-Content-Type-Options', 'nosniff');
|
|
68
|
+
res.set('X-Frame-Options', 'DENY');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
createAppServer(appServerConfig)
|
|
@@ -61,6 +78,9 @@ class AppServerFactory
|
|
|
61
78
|
if(this.useHelmet){
|
|
62
79
|
this.app.use(this.helmetConfig ? helmet(this.helmetConfig) : helmet());
|
|
63
80
|
}
|
|
81
|
+
if(this.useVirtualHosts){
|
|
82
|
+
this.setupVirtualHosts();
|
|
83
|
+
}
|
|
64
84
|
if(this.useCors){
|
|
65
85
|
let corsOptions = {
|
|
66
86
|
origin: this.corsOrigin,
|
|
@@ -89,12 +109,8 @@ class AppServerFactory
|
|
|
89
109
|
if(!req.body){
|
|
90
110
|
return next();
|
|
91
111
|
}
|
|
92
|
-
if(typeof req.body
|
|
93
|
-
|
|
94
|
-
if(typeof req.body[key] === 'string'){
|
|
95
|
-
req.body[key] = sanitizeHtml(req.body[key]);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
112
|
+
if('object' === typeof req.body){
|
|
113
|
+
this.sanitizeRequestBody(req.body);
|
|
98
114
|
}
|
|
99
115
|
next();
|
|
100
116
|
});
|
|
@@ -102,7 +118,7 @@ class AppServerFactory
|
|
|
102
118
|
if(this.useExpressJson){
|
|
103
119
|
this.app.use(this.applicationFramework.json({
|
|
104
120
|
limit: this.jsonLimit,
|
|
105
|
-
verify: this.verifyContentTypeJson
|
|
121
|
+
verify: this.verifyContentTypeJson.bind(this)
|
|
106
122
|
}));
|
|
107
123
|
}
|
|
108
124
|
if(this.useUrlencoded){
|
|
@@ -115,20 +131,90 @@ class AppServerFactory
|
|
|
115
131
|
this.app.enable('trust proxy', this.trustedProxy);
|
|
116
132
|
}
|
|
117
133
|
this.appServer = this.createServer();
|
|
134
|
+
if(!this.appServer){
|
|
135
|
+
this.error = {message: 'Failed to create app server'};
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if(this.autoListen){
|
|
139
|
+
this.listen();
|
|
140
|
+
}
|
|
118
141
|
return {app: this.app, appServer: this.appServer};
|
|
119
142
|
}
|
|
120
143
|
|
|
144
|
+
sanitizeRequestBody(body)
|
|
145
|
+
{
|
|
146
|
+
let bodyKeys = Object.keys(body);
|
|
147
|
+
for(let i = 0; i < bodyKeys.length; i++){
|
|
148
|
+
let key = bodyKeys[i];
|
|
149
|
+
if('string' === typeof body[key]){
|
|
150
|
+
body[key] = sanitizeHtml(body[key], this.sanitizeOptions);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if('object' === typeof body[key] && null !== body[key]){
|
|
154
|
+
this.sanitizeRequestBody(body[key]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
121
159
|
verifyContentTypeJson(req, res, buf)
|
|
122
160
|
{
|
|
123
161
|
let contentType = req.headers['content-type'] || '';
|
|
124
162
|
if(
|
|
125
|
-
req.method
|
|
163
|
+
'POST' === req.method
|
|
126
164
|
&& 0 < buf.length
|
|
127
165
|
&& !contentType.includes('application/json')
|
|
128
166
|
&& !contentType.includes('multipart/form-data')
|
|
129
167
|
){
|
|
130
|
-
|
|
168
|
+
this.error = {message: 'Invalid content-type for JSON request'};
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setupVirtualHosts()
|
|
174
|
+
{
|
|
175
|
+
if(0 === this.domains.length){
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.app.use((req, res, next) => {
|
|
179
|
+
let hostname = req.get('host');
|
|
180
|
+
if(!hostname){
|
|
181
|
+
if(this.defaultDomain){
|
|
182
|
+
req.domain = this.defaultDomain;
|
|
183
|
+
return next();
|
|
184
|
+
}
|
|
185
|
+
this.error = {message: 'No hostname provided and no default domain configured'};
|
|
186
|
+
return res.status(400).send('Bad Request');
|
|
187
|
+
}
|
|
188
|
+
let domain = this.findDomainConfig(hostname);
|
|
189
|
+
if(!domain){
|
|
190
|
+
if(this.defaultDomain){
|
|
191
|
+
req.domain = this.defaultDomain;
|
|
192
|
+
return next();
|
|
193
|
+
}
|
|
194
|
+
this.error = {message: 'Unknown domain: ' + hostname};
|
|
195
|
+
return res.status(404).send('Domain not found');
|
|
196
|
+
}
|
|
197
|
+
req.domain = domain;
|
|
198
|
+
next();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
findDomainConfig(hostname)
|
|
203
|
+
{
|
|
204
|
+
if(!hostname || 'string' !== typeof hostname){
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
let cleanHostname = hostname.toLowerCase().trim();
|
|
208
|
+
for(let i = 0; i < this.domains.length; i++){
|
|
209
|
+
let domain = this.domains[i];
|
|
210
|
+
if(domain.hostname === cleanHostname){
|
|
211
|
+
return domain;
|
|
212
|
+
}
|
|
213
|
+
if(domain.aliases && domain.aliases.includes(cleanHostname)){
|
|
214
|
+
return domain;
|
|
215
|
+
}
|
|
131
216
|
}
|
|
217
|
+
return false;
|
|
132
218
|
}
|
|
133
219
|
|
|
134
220
|
createServer()
|
|
@@ -136,19 +222,25 @@ class AppServerFactory
|
|
|
136
222
|
if(!this.useHttps){
|
|
137
223
|
return http.createServer(this.app);
|
|
138
224
|
}
|
|
225
|
+
if(this.useVirtualHosts && 0 < this.domains.length){
|
|
226
|
+
return this.createHttpsServerWithSNI();
|
|
227
|
+
}
|
|
228
|
+
return this.createSingleHttpsServer();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
createSingleHttpsServer()
|
|
232
|
+
{
|
|
139
233
|
let key = FileHandler.readFile(this.keyPath, 'Key');
|
|
140
234
|
if(!key){
|
|
235
|
+
this.error = {message: 'Could not read SSL key file: ' + this.keyPath};
|
|
141
236
|
return false;
|
|
142
237
|
}
|
|
143
238
|
let cert = FileHandler.readFile(this.certPath, 'Cert');
|
|
144
239
|
if(!cert){
|
|
240
|
+
this.error = {message: 'Could not read SSL certificate file: ' + this.certPath};
|
|
145
241
|
return false;
|
|
146
242
|
}
|
|
147
|
-
let credentials = {
|
|
148
|
-
key: key.toString(),
|
|
149
|
-
cert: cert.toString(),
|
|
150
|
-
passphrase: this.passphrase
|
|
151
|
-
};
|
|
243
|
+
let credentials = {key, cert, passphrase: this.passphrase};
|
|
152
244
|
if('' !== this.httpsChain){
|
|
153
245
|
let ca = FileHandler.readFile(this.httpsChain, 'Certificate Authority');
|
|
154
246
|
if(ca){
|
|
@@ -158,6 +250,60 @@ class AppServerFactory
|
|
|
158
250
|
return https.createServer(credentials, this.app);
|
|
159
251
|
}
|
|
160
252
|
|
|
253
|
+
createHttpsServerWithSNI()
|
|
254
|
+
{
|
|
255
|
+
let defaultCredentials = this.loadDefaultCredentials();
|
|
256
|
+
if(!defaultCredentials){
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
let httpsOptions = Object.assign({}, defaultCredentials);
|
|
260
|
+
httpsOptions.SNICallback = (hostname, callback) => {
|
|
261
|
+
let domain = this.findDomainConfig(hostname);
|
|
262
|
+
if(!domain || !domain.keyPath || !domain.certPath){
|
|
263
|
+
return callback(null, null);
|
|
264
|
+
}
|
|
265
|
+
let key = FileHandler.readFile(domain.keyPath, 'Domain Key');
|
|
266
|
+
if(!key){
|
|
267
|
+
this.error = {message: 'Could not read domain SSL key: '+domain.keyPath};
|
|
268
|
+
return callback(null, null);
|
|
269
|
+
}
|
|
270
|
+
let cert = FileHandler.readFile(domain.certPath, 'Domain Cert');
|
|
271
|
+
if(!cert){
|
|
272
|
+
this.error = {message: 'Could not read domain SSL certificate: '+domain.certPath};
|
|
273
|
+
return callback(null, null);
|
|
274
|
+
}
|
|
275
|
+
let ctx = require('tls').createSecureContext({key, cert});
|
|
276
|
+
callback(null, ctx);
|
|
277
|
+
};
|
|
278
|
+
return https.createServer(httpsOptions, this.app);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
loadDefaultCredentials()
|
|
282
|
+
{
|
|
283
|
+
let key = FileHandler.readFile(this.keyPath, 'Default Key');
|
|
284
|
+
if(!key){
|
|
285
|
+
this.error = {message: 'Could not read default SSL key file: '+this.keyPath};
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
let cert = FileHandler.readFile(this.certPath, 'Default Cert');
|
|
289
|
+
if(!cert){
|
|
290
|
+
this.error = {message: 'Could not read default SSL certificate file: '+this.certPath};
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return {key, cert, passphrase: this.passphrase};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
listen(port)
|
|
297
|
+
{
|
|
298
|
+
let listenPort = port || this.port;
|
|
299
|
+
if(!this.appServer){
|
|
300
|
+
this.error = {message: 'Cannot listen: app server not created'};
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
this.appServer.listen(listenPort);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
161
307
|
async enableServeHome(app, homePageLoadCallback)
|
|
162
308
|
{
|
|
163
309
|
let limiterParams = {
|
|
@@ -189,16 +335,16 @@ class AppServerFactory
|
|
|
189
335
|
}
|
|
190
336
|
return res.status(500).send(errorMessage);
|
|
191
337
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.error = {message, error};
|
|
338
|
+
let homepageContent = await homePageLoadCallback(req);
|
|
339
|
+
if(!homepageContent){
|
|
340
|
+
let message = 'Error loading homepage content';
|
|
341
|
+
this.error = {message};
|
|
197
342
|
if('function' === typeof this.processErrorResponse){
|
|
198
343
|
return this.processErrorResponse(500, message, req, res);
|
|
199
344
|
}
|
|
200
345
|
return res.status(500).send(message);
|
|
201
346
|
}
|
|
347
|
+
return res.send(homepageContent);
|
|
202
348
|
}
|
|
203
349
|
next();
|
|
204
350
|
});
|
|
@@ -206,38 +352,84 @@ class AppServerFactory
|
|
|
206
352
|
|
|
207
353
|
async serveStatics(app, statics)
|
|
208
354
|
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
let staticOptions = {
|
|
214
|
-
maxAge: '1d',
|
|
215
|
-
etag: true,
|
|
216
|
-
lastModified: true,
|
|
217
|
-
index: false,
|
|
218
|
-
setHeaders: function(res){
|
|
219
|
-
res.set('X-Content-Type-Options', 'nosniff');
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
app.use(this.applicationFramework.static(statics, staticOptions));
|
|
355
|
+
app.use(this.applicationFramework.static(statics, this.staticOptions));
|
|
356
|
+
return true;
|
|
223
357
|
}
|
|
224
358
|
|
|
225
359
|
async serveStaticsPath(app, staticsPath, statics)
|
|
226
360
|
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
361
|
+
app.use(staticsPath, this.applicationFramework.static(statics, this.staticOptions));
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
addDomain(domainConfig)
|
|
366
|
+
{
|
|
367
|
+
if(!domainConfig || !domainConfig.hostname){
|
|
368
|
+
this.error = {message: 'Domain configuration missing hostname'};
|
|
369
|
+
return false;
|
|
230
370
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
371
|
+
if('string' !== typeof domainConfig.hostname){
|
|
372
|
+
this.error = {message: 'Domain hostname must be a string'};
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
this.domains.push(domainConfig);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async close()
|
|
380
|
+
{
|
|
381
|
+
if(!this.appServer){
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
return this.appServer.close();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
enableCSP(cspOptions)
|
|
388
|
+
{
|
|
389
|
+
let defaults = {
|
|
390
|
+
'default-src': ["'self'"],
|
|
391
|
+
'script-src': ["'self'"],
|
|
392
|
+
'style-src': ["'self'", "'unsafe-inline'"],
|
|
393
|
+
'img-src': ["'self'", "data:", "https:"],
|
|
394
|
+
'font-src': ["'self'"],
|
|
395
|
+
'connect-src': ["'self'"],
|
|
396
|
+
'frame-ancestors': ["'none'"],
|
|
397
|
+
'base-uri': ["'self'"],
|
|
398
|
+
'form-action': ["'self'"]
|
|
399
|
+
};
|
|
400
|
+
let csp = Object.assign({}, defaults, cspOptions);
|
|
401
|
+
let policyString = '';
|
|
402
|
+
let keys = Object.keys(csp);
|
|
403
|
+
for(let i = 0; i < keys.length; i++){
|
|
404
|
+
let directive = keys[i];
|
|
405
|
+
let sources = csp[directive];
|
|
406
|
+
if(0 < i){
|
|
407
|
+
policyString += '; ';
|
|
238
408
|
}
|
|
409
|
+
policyString += directive + ' ' + sources.join(' ');
|
|
410
|
+
}
|
|
411
|
+
this.app.use((req, res, next) => {
|
|
412
|
+
res.setHeader('Content-Security-Policy', policyString);
|
|
413
|
+
next();
|
|
414
|
+
});
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
validateInput(input, type)
|
|
419
|
+
{
|
|
420
|
+
if('string' !== typeof input){
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
let patterns = {
|
|
424
|
+
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
425
|
+
username: /^[a-zA-Z0-9_-]{3,30}$/,
|
|
426
|
+
strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
|
427
|
+
alphanumeric: /^[a-zA-Z0-9]+$/,
|
|
428
|
+
numeric: /^\d+$/,
|
|
429
|
+
hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
|
430
|
+
ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
239
431
|
};
|
|
240
|
-
|
|
432
|
+
return patterns[type] ? patterns[type].test(input) : false;
|
|
241
433
|
}
|
|
242
434
|
|
|
243
435
|
}
|
package/lib/encryptor.js
CHANGED
|
@@ -4,37 +4,44 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const crypto = require('crypto');
|
|
8
8
|
|
|
9
9
|
class Encryptor
|
|
10
10
|
{
|
|
11
11
|
|
|
12
12
|
constructor()
|
|
13
13
|
{
|
|
14
|
-
|
|
15
|
-
this.iterations = 60000;
|
|
14
|
+
this.iterations = 100000;
|
|
16
15
|
this.keylen = 64;
|
|
17
16
|
this.digest = 'sha512';
|
|
17
|
+
this.saltLength = 32;
|
|
18
|
+
this.algorithm = 'aes-256-gcm';
|
|
19
|
+
this.ivLength = 16;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
encryptPassword(password)
|
|
21
23
|
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if(!password || 'string' !== typeof password){
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
let salt = crypto.randomBytes(this.saltLength);
|
|
28
|
+
let hash = crypto.pbkdf2Sync(password, salt, this.iterations, this.keylen, this.digest);
|
|
29
|
+
return salt.toString('hex') + ':' + hash.toString('hex');
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
validatePassword(password, storedPassword)
|
|
29
33
|
{
|
|
34
|
+
if(!password || !storedPassword || 'string' !== typeof password || 'string' !== typeof storedPassword){
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
30
37
|
let parts = storedPassword.split(':');
|
|
31
38
|
if(2 !== parts.length){
|
|
32
39
|
return false;
|
|
33
40
|
}
|
|
34
|
-
let salt = parts[0];
|
|
41
|
+
let salt = Buffer.from(parts[0], 'hex');
|
|
35
42
|
let storedHash = parts[1];
|
|
36
|
-
let hash = crypto.pbkdf2Sync(password, salt, this.iterations, this.keylen, this.digest)
|
|
37
|
-
return
|
|
43
|
+
let hash = crypto.pbkdf2Sync(password, salt, this.iterations, this.keylen, this.digest);
|
|
44
|
+
return hash.toString('hex') === storedHash;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
generateSecretKey()
|
|
@@ -42,6 +49,114 @@ class Encryptor
|
|
|
42
49
|
return crypto.randomBytes(32).toString('hex');
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
encryptData(data, key)
|
|
53
|
+
{
|
|
54
|
+
if(!data || !key){
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
let iv = crypto.randomBytes(this.ivLength);
|
|
58
|
+
let cipher = crypto.createCipher(this.algorithm, key, iv);
|
|
59
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
60
|
+
encrypted += cipher.final('hex');
|
|
61
|
+
let authTag = cipher.getAuthTag();
|
|
62
|
+
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
decryptData(encryptedData, key)
|
|
66
|
+
{
|
|
67
|
+
if(!encryptedData || !key || 'string' !== typeof encryptedData){
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
let parts = encryptedData.split(':');
|
|
71
|
+
if(3 !== parts.length){
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
let iv = Buffer.from(parts[0], 'hex');
|
|
76
|
+
let authTag = Buffer.from(parts[1], 'hex');
|
|
77
|
+
let encrypted = parts[2];
|
|
78
|
+
let decipher = crypto.createDecipher(this.algorithm, key, iv);
|
|
79
|
+
decipher.setAuthTag(authTag);
|
|
80
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
81
|
+
decrypted += decipher.final('utf8');
|
|
82
|
+
return decrypted;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
generateSecureToken(length = 32)
|
|
89
|
+
{
|
|
90
|
+
if(0 >= length || 256 < length){
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return crypto.randomBytes(length).toString('base64url');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
generateTOTP(secret, timeStep = 30)
|
|
97
|
+
{
|
|
98
|
+
if(!secret || 'string' !== typeof secret){
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let time = Math.floor(Date.now() / 1000 / timeStep);
|
|
102
|
+
let timeBuffer = Buffer.allocUnsafe(8);
|
|
103
|
+
timeBuffer.writeUInt32BE(0, 0);
|
|
104
|
+
timeBuffer.writeUInt32BE(time, 4);
|
|
105
|
+
let hmac = crypto.createHmac('sha1', Buffer.from(secret, 'base32'));
|
|
106
|
+
hmac.update(timeBuffer);
|
|
107
|
+
let digest = hmac.digest();
|
|
108
|
+
let offset = digest[digest.length - 1] & 0x0f;
|
|
109
|
+
let code = (digest.readUInt32BE(offset) & 0x7fffffff) % 1000000;
|
|
110
|
+
return code.toString().padStart(6, '0');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
hashData(data, algorithm = 'sha256')
|
|
114
|
+
{
|
|
115
|
+
if(!data){
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
let validAlgorithms = ['sha256', 'sha512', 'md5'];
|
|
119
|
+
if(-1 === validAlgorithms.indexOf(algorithm)){
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return crypto.createHash(algorithm).update(data).digest('hex');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
generateHMAC(data, secret, algorithm = 'sha256')
|
|
126
|
+
{
|
|
127
|
+
if(!data || !secret){
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
let validAlgorithms = ['sha256', 'sha512'];
|
|
131
|
+
if(-1 === validAlgorithms.indexOf(algorithm)){
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return crypto.createHmac(algorithm, secret).update(data).digest('hex');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
verifyHMAC(data, secret, signature, algorithm = 'sha256')
|
|
138
|
+
{
|
|
139
|
+
if(!data || !secret || !signature){
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
let expectedSignature = this.generateHMAC(data, secret, algorithm);
|
|
143
|
+
if(!expectedSignature){
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
constantTimeCompare(a, b)
|
|
150
|
+
{
|
|
151
|
+
if(!a || !b || 'string' !== typeof a || 'string' !== typeof b){
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if(b.length !== a.length){
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
158
|
+
}
|
|
159
|
+
|
|
45
160
|
}
|
|
46
161
|
|
|
47
162
|
module.exports.Encryptor = new Encryptor();
|
package/lib/file-handler.js
CHANGED
|
@@ -15,6 +15,13 @@ class FileHandler
|
|
|
15
15
|
this.encoding = (process.env.RELDENS_DEFAULT_ENCODING || 'utf8');
|
|
16
16
|
this.sep = path.sep;
|
|
17
17
|
this.error = {message: ''};
|
|
18
|
+
this.maxPathLength = 2048;
|
|
19
|
+
this.dangerousPatterns = [
|
|
20
|
+
'../', '..\\', './', '.\\',
|
|
21
|
+
'/etc/', '/proc/', '/sys/',
|
|
22
|
+
'C:\\Windows\\', 'C:\\System32\\',
|
|
23
|
+
'%2e%2e%2f', '%2e%2e%5c'
|
|
24
|
+
];
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
joinPaths(...args)
|
|
@@ -37,7 +44,16 @@ class FileHandler
|
|
|
37
44
|
return false;
|
|
38
45
|
}
|
|
39
46
|
let pathStr = String(filePath);
|
|
40
|
-
|
|
47
|
+
if(this.maxPathLength < pathStr.length){
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
let normalized = pathStr.replace(/\\/g, '/');
|
|
51
|
+
for(let pattern of this.dangerousPatterns){
|
|
52
|
+
if(normalized.toLowerCase().includes(pattern.toLowerCase())){
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
sanitizePath(filePath)
|
|
@@ -48,7 +64,7 @@ class FileHandler
|
|
|
48
64
|
return String(filePath)
|
|
49
65
|
.replace(/\.\./g, '')
|
|
50
66
|
.replace(/[:*?"<>|]/g, '')
|
|
51
|
-
.substring(0,
|
|
67
|
+
.substring(0, this.maxPathLength);
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
generateSecureFilename(originalName)
|
|
@@ -65,52 +81,49 @@ class FileHandler
|
|
|
65
81
|
|
|
66
82
|
remove(fullPath)
|
|
67
83
|
{
|
|
84
|
+
let deletePath = Array.isArray(fullPath) ? this.joinPaths(...fullPath) : fullPath;
|
|
85
|
+
if(!this.exists(deletePath)){
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
68
88
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
let deletePath = Array.isArray(fullPath) ? this.joinPaths(...fullPath) : fullPath;
|
|
74
|
-
if(fs.existsSync(deletePath)){
|
|
75
|
-
fs.rmSync(deletePath, {recursive: true, force: true});
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
89
|
+
fs.rmSync(deletePath, {recursive: true, force: true});
|
|
90
|
+
return true;
|
|
78
91
|
} catch (error) {
|
|
79
92
|
this.error = {message: 'Failed to remove folder.', error, fullPath};
|
|
93
|
+
return false;
|
|
80
94
|
}
|
|
81
|
-
return false;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
createFolder(folderPath)
|
|
85
98
|
{
|
|
99
|
+
if(!this.isValidPath(folderPath)){
|
|
100
|
+
this.error = {message: 'Invalid folder path.', folderPath};
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if(this.exists(folderPath)){
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
86
106
|
try {
|
|
87
|
-
if(!this.isValidPath(folderPath)){
|
|
88
|
-
this.error = {message: 'Invalid folder path.', folderPath};
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
if(fs.existsSync(folderPath)){
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
107
|
fs.mkdirSync(folderPath, {recursive: true});
|
|
95
108
|
return true;
|
|
96
109
|
} catch (error) {
|
|
97
110
|
this.error = {message: 'Failed to create folder.', error, folderPath};
|
|
111
|
+
return false;
|
|
98
112
|
}
|
|
99
|
-
return false;
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
copyFolderSync(from, to)
|
|
103
116
|
{
|
|
117
|
+
if(!this.isValidPath(from) || !this.isValidPath(to)){
|
|
118
|
+
this.error = {message: 'Invalid path for folder copy.', from, to};
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
104
121
|
try {
|
|
105
|
-
if(!this.isValidPath(from) || !this.isValidPath(to)){
|
|
106
|
-
this.error = {message: 'Invalid path for folder copy.', from, to};
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
122
|
fs.mkdirSync(to, {recursive: true});
|
|
110
123
|
let folders = fs.readdirSync(from);
|
|
111
124
|
for(let element of folders){
|
|
112
125
|
let elementPath = path.join(from, element);
|
|
113
|
-
if(!
|
|
126
|
+
if(!this.exists(elementPath)){
|
|
114
127
|
continue;
|
|
115
128
|
}
|
|
116
129
|
if(fs.lstatSync(elementPath).isFile()){
|
|
@@ -122,8 +135,8 @@ class FileHandler
|
|
|
122
135
|
return true;
|
|
123
136
|
} catch (error) {
|
|
124
137
|
this.error = {message: 'Failed to copy folder.', error, from, to};
|
|
138
|
+
return false;
|
|
125
139
|
}
|
|
126
|
-
return false;
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
copyFileSyncIfDoesNotExist(from, to)
|
|
@@ -132,9 +145,16 @@ class FileHandler
|
|
|
132
145
|
this.error = {message: 'Invalid path for file copy.', from, to};
|
|
133
146
|
return false;
|
|
134
147
|
}
|
|
135
|
-
if(!
|
|
136
|
-
|
|
148
|
+
if(!this.exists(to)){
|
|
149
|
+
try {
|
|
150
|
+
fs.copyFileSync(from, to);
|
|
151
|
+
return true;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.error = {message: 'Failed to copy file.', error, from, to};
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
137
156
|
}
|
|
157
|
+
return true;
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
copyFile(from, to)
|
|
@@ -154,8 +174,8 @@ class FileHandler
|
|
|
154
174
|
return true;
|
|
155
175
|
} catch (error) {
|
|
156
176
|
this.error = {message: 'Failed to copy file.', error, from, to, origin, dest};
|
|
177
|
+
return false;
|
|
157
178
|
}
|
|
158
|
-
return false;
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
extension(filePath)
|
|
@@ -199,8 +219,55 @@ class FileHandler
|
|
|
199
219
|
return fs.lstatSync(filePath).isFile();
|
|
200
220
|
} catch (error) {
|
|
201
221
|
this.error = {message: 'Can not check file.', error, filePath};
|
|
222
|
+
return false;
|
|
202
223
|
}
|
|
203
|
-
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
isFolder(dirPath)
|
|
227
|
+
{
|
|
228
|
+
if(!this.isValidPath(dirPath)){
|
|
229
|
+
this.error = {message: 'Invalid folder path.', dirPath};
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if(!this.exists(dirPath)){
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
return fs.lstatSync(dirPath).isDirectory();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.error = {message: 'Can not check folder.', error, dirPath};
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getFilesInFolder(dirPath, extensions = [])
|
|
244
|
+
{
|
|
245
|
+
if(!this.isValidPath(dirPath)){
|
|
246
|
+
this.error = {message: 'Invalid folder path.', dirPath};
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
let files = this.readFolder(dirPath);
|
|
250
|
+
if(0 === files.length){
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
let result = [];
|
|
254
|
+
for(let file of files){
|
|
255
|
+
let filePath = path.join(dirPath, file);
|
|
256
|
+
if(!this.isFile(filePath)){
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if(0 === extensions.length){
|
|
260
|
+
result.push(file);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for(let ext of extensions){
|
|
264
|
+
if(file.endsWith(ext)){
|
|
265
|
+
result.push(file);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
204
271
|
}
|
|
205
272
|
|
|
206
273
|
permissionsCheck(systemPath)
|
|
@@ -249,17 +316,12 @@ class FileHandler
|
|
|
249
316
|
this.error = {message: 'File check failed to fetch file contents.', filePath};
|
|
250
317
|
return false;
|
|
251
318
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.error = {message: 'Can not read data or empty file.', filePath};
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
return fileContent;
|
|
259
|
-
} catch(error){
|
|
260
|
-
this.error = {message: 'Error reading file.', filePath, error};
|
|
319
|
+
let fileContent = this.readFile(filePath);
|
|
320
|
+
if(!fileContent){
|
|
321
|
+
this.error = {message: 'Can not read data or empty file.', filePath};
|
|
261
322
|
return false;
|
|
262
323
|
}
|
|
324
|
+
return fileContent;
|
|
263
325
|
}
|
|
264
326
|
|
|
265
327
|
readFile(filePath)
|
|
@@ -287,7 +349,8 @@ class FileHandler
|
|
|
287
349
|
return false;
|
|
288
350
|
}
|
|
289
351
|
try {
|
|
290
|
-
|
|
352
|
+
fs.writeFileSync(fs.openSync(filePath, 'w+'), contents);
|
|
353
|
+
return true;
|
|
291
354
|
} catch(error){
|
|
292
355
|
this.error = {message: 'Error updating file.', filePath, error};
|
|
293
356
|
return false;
|
|
@@ -305,8 +368,8 @@ class FileHandler
|
|
|
305
368
|
return true;
|
|
306
369
|
} catch (error) {
|
|
307
370
|
this.error = {message: 'Error saving the file.', fileName, error};
|
|
371
|
+
return false;
|
|
308
372
|
}
|
|
309
|
-
return false;
|
|
310
373
|
}
|
|
311
374
|
|
|
312
375
|
validateFileType(filePath, allowedType, allowedFileTypes, maxFileSize)
|
|
@@ -369,6 +432,54 @@ class FileHandler
|
|
|
369
432
|
}
|
|
370
433
|
}
|
|
371
434
|
|
|
435
|
+
detectFileType(filePath)
|
|
436
|
+
{
|
|
437
|
+
let buffer = this.getFirstFileBytes(filePath, 16);
|
|
438
|
+
if(!buffer){
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
let signatures = {
|
|
442
|
+
'image/jpeg': [0xFF, 0xD8, 0xFF],
|
|
443
|
+
'image/png': [0x89, 0x50, 0x4E, 0x47],
|
|
444
|
+
'image/gif': [0x47, 0x49, 0x46, 0x38],
|
|
445
|
+
'application/pdf': [0x25, 0x50, 0x44, 0x46],
|
|
446
|
+
'application/zip': [0x50, 0x4B, 0x03, 0x04]
|
|
447
|
+
};
|
|
448
|
+
for(let mimeType of Object.keys(signatures)){
|
|
449
|
+
let signature = signatures[mimeType];
|
|
450
|
+
let matches = true;
|
|
451
|
+
for(let i = 0; i < signature.length; i++){
|
|
452
|
+
if(buffer[i] !== signature[i]){
|
|
453
|
+
matches = false;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if(matches){
|
|
458
|
+
return mimeType;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return 'application/octet-stream';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
quarantineFile(filePath, reason = 'security')
|
|
465
|
+
{
|
|
466
|
+
let quarantineDir = this.joinPaths(process.cwd(), 'quarantine');
|
|
467
|
+
this.createFolder(quarantineDir);
|
|
468
|
+
let timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
469
|
+
let quarantinePath = this.joinPaths(
|
|
470
|
+
quarantineDir,
|
|
471
|
+
timestamp + '-' + reason + '-' + path.basename(filePath)
|
|
472
|
+
);
|
|
473
|
+
return this.copyFile(filePath, quarantinePath);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
createTempFile(prefix = 'temp', extension = '.tmp')
|
|
477
|
+
{
|
|
478
|
+
let tempDir = require('os').tmpdir();
|
|
479
|
+
let fileName = prefix + '-' + this.generateSecureFilename('file' + extension);
|
|
480
|
+
return this.joinPaths(tempDir, fileName);
|
|
481
|
+
}
|
|
482
|
+
|
|
372
483
|
}
|
|
373
484
|
|
|
374
485
|
module.exports.FileHandler = new FileHandler();
|
package/lib/uploader-factory.js
CHANGED
|
@@ -19,12 +19,17 @@ class UploaderFactory
|
|
|
19
19
|
this.allowedExtensions = props.allowedExtensions;
|
|
20
20
|
this.applySecureFileNames = props.applySecureFileNames;
|
|
21
21
|
this.processErrorResponse = props.processErrorResponse || false;
|
|
22
|
+
this.dangerousExtensions = props.dangerousExtensions !== undefined
|
|
23
|
+
? props.dangerousExtensions
|
|
24
|
+
: ['.exe', '.bat', '.cmd', '.com', '.scr', '.pif', '.vbs', '.js'];
|
|
25
|
+
this.maxFilenameLength = props.maxFilenameLength || 255;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
createUploader(fields, buckets, allowedFileTypes)
|
|
25
29
|
{
|
|
26
30
|
if(!this.validateInputs(fields, buckets, allowedFileTypes)){
|
|
27
|
-
|
|
31
|
+
this.error = {message: 'Invalid uploader configuration: ' + this.error.message};
|
|
32
|
+
return false;
|
|
28
33
|
}
|
|
29
34
|
let diskStorageConfiguration = {
|
|
30
35
|
destination: (req, file, cb) => {
|
|
@@ -36,7 +41,10 @@ class UploaderFactory
|
|
|
36
41
|
cb(null, dest);
|
|
37
42
|
}
|
|
38
43
|
};
|
|
39
|
-
diskStorageConfiguration
|
|
44
|
+
diskStorageConfiguration.filename = (req, file, cb) => {
|
|
45
|
+
if(!this.validateFilenameSecurity(file.originalname)){
|
|
46
|
+
return cb(new Error('Invalid filename'));
|
|
47
|
+
}
|
|
40
48
|
if(!this.applySecureFileNames) {
|
|
41
49
|
cb(null, file.originalname);
|
|
42
50
|
return;
|
|
@@ -53,7 +61,7 @@ class UploaderFactory
|
|
|
53
61
|
fileSize: this.maxFileSize
|
|
54
62
|
};
|
|
55
63
|
if(0 < this.fileLimit){
|
|
56
|
-
limits
|
|
64
|
+
limits.files = this.fileLimit;
|
|
57
65
|
}
|
|
58
66
|
let upload = multer({
|
|
59
67
|
storage,
|
|
@@ -95,60 +103,84 @@ class UploaderFactory
|
|
|
95
103
|
if(!req.files){
|
|
96
104
|
return next();
|
|
97
105
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for(let file of req.files[fieldName]){
|
|
101
|
-
if(!await this.validateFileContents(file, allowedFileTypes[fieldName])){
|
|
102
|
-
if(FileHandler.exists(file.path)){
|
|
103
|
-
FileHandler.remove(file.path);
|
|
104
|
-
}
|
|
105
|
-
let messageContents = 'File contents do not match declared type.';
|
|
106
|
-
if('function' === typeof this.processErrorResponse){
|
|
107
|
-
return this.processErrorResponse(415, messageContents, req, res);
|
|
108
|
-
}
|
|
109
|
-
return res.status(415).send(messageContents);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
next();
|
|
114
|
-
} catch(error){
|
|
115
|
-
let messageProcessing = 'Error processing uploaded files.';
|
|
116
|
-
this.error = {message: messageProcessing, error};
|
|
106
|
+
let validationResult = await this.validateAllUploadedFiles(req, allowedFileTypes);
|
|
107
|
+
if(!validationResult){
|
|
117
108
|
this.cleanupFiles(req.files);
|
|
109
|
+
let messageContents = 'File validation failed.';
|
|
118
110
|
if('function' === typeof this.processErrorResponse){
|
|
119
|
-
return this.processErrorResponse(
|
|
111
|
+
return this.processErrorResponse(415, messageContents, req, res);
|
|
120
112
|
}
|
|
121
|
-
return res.status(
|
|
113
|
+
return res.status(415).send(messageContents);
|
|
122
114
|
}
|
|
115
|
+
next();
|
|
123
116
|
});
|
|
124
117
|
};
|
|
125
118
|
}
|
|
126
119
|
|
|
120
|
+
async validateAllUploadedFiles(req, allowedFileTypes)
|
|
121
|
+
{
|
|
122
|
+
try {
|
|
123
|
+
for(let fieldName in req.files){
|
|
124
|
+
for(let file of req.files[fieldName]){
|
|
125
|
+
if(!await this.validateFileContents(file, allowedFileTypes[fieldName])){
|
|
126
|
+
FileHandler.remove(file.path);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
} catch(error){
|
|
133
|
+
this.error = {message: 'Error processing uploaded files.', error};
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
validateFilenameSecurity(filename)
|
|
139
|
+
{
|
|
140
|
+
if(!filename || 'string' !== typeof filename){
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if(this.maxFilenameLength < filename.length){
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
let ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
|
|
147
|
+
if(-1 !== this.dangerousExtensions.indexOf(ext)){
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
let dangerous = ['../', '..\\', '/', '\\', '<', '>', ':', '*', '?', '"', '|'];
|
|
151
|
+
for(let char of dangerous){
|
|
152
|
+
if(-1 !== filename.indexOf(char)){
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
validateInputs(fields, buckets, allowedFileTypes)
|
|
128
160
|
{
|
|
129
161
|
if(!Array.isArray(fields)){
|
|
130
162
|
this.error = {message: 'Fields must be an array'};
|
|
131
163
|
return false;
|
|
132
164
|
}
|
|
133
|
-
if(!buckets || typeof buckets
|
|
165
|
+
if(!buckets || 'object' !== typeof buckets){
|
|
134
166
|
this.error = {message: 'Buckets must be an object'};
|
|
135
167
|
return false;
|
|
136
168
|
}
|
|
137
|
-
if(!allowedFileTypes || typeof allowedFileTypes
|
|
169
|
+
if(!allowedFileTypes || 'object' !== typeof allowedFileTypes){
|
|
138
170
|
this.error = {message: 'AllowedFileTypes must be an object'};
|
|
139
171
|
return false;
|
|
140
172
|
}
|
|
141
173
|
for(let field of fields){
|
|
142
|
-
if(!field.name || typeof field.name
|
|
174
|
+
if(!field.name || 'string' !== typeof field.name){
|
|
143
175
|
this.error = {message: 'Field name is invalid'};
|
|
144
176
|
return false;
|
|
145
177
|
}
|
|
146
178
|
if(!buckets[field.name]){
|
|
147
|
-
this.error = {message:
|
|
179
|
+
this.error = {message: 'Missing bucket for field: ' + field.name};
|
|
148
180
|
return false;
|
|
149
181
|
}
|
|
150
182
|
if(!allowedFileTypes[field.name]){
|
|
151
|
-
this.error = {message:
|
|
183
|
+
this.error = {message: 'Missing allowedFileType for field: ' + field.name};
|
|
152
184
|
return false;
|
|
153
185
|
}
|
|
154
186
|
}
|
|
@@ -158,39 +190,49 @@ class UploaderFactory
|
|
|
158
190
|
validateFile(file, allowedFileType, cb)
|
|
159
191
|
{
|
|
160
192
|
if(!allowedFileType){
|
|
161
|
-
return cb(
|
|
193
|
+
return cb();
|
|
194
|
+
}
|
|
195
|
+
if(!this.validateFilenameSecurity(file.originalname)){
|
|
196
|
+
this.error = {message: 'Insecure filename: ' + file.originalname};
|
|
197
|
+
return cb(new Error('Insecure filename: ' + file.originalname));
|
|
162
198
|
}
|
|
163
199
|
let fileExtension = FileHandler.extension(file.originalname).toLowerCase();
|
|
164
200
|
let allowedExtensions = this.allowedExtensions[allowedFileType];
|
|
165
201
|
if(allowedExtensions && !allowedExtensions.includes(fileExtension)){
|
|
166
|
-
this.error = {message:
|
|
167
|
-
return cb(
|
|
202
|
+
this.error = {message: 'Invalid file extension: ' + fileExtension};
|
|
203
|
+
return cb(new Error('Invalid file extension: ' + fileExtension));
|
|
168
204
|
}
|
|
169
205
|
let allowedFileTypeRegex = this.convertToRegex(allowedFileType);
|
|
170
206
|
if(!allowedFileTypeRegex){
|
|
171
207
|
this.error = {message: 'File type could not be converted to regex.', allowedFileType};
|
|
172
|
-
return cb(
|
|
208
|
+
return cb(new Error('File type could not be converted to regex'));
|
|
173
209
|
}
|
|
174
210
|
let mimeTypeValid = allowedFileTypeRegex.test(file.mimetype);
|
|
175
211
|
if(!mimeTypeValid){
|
|
176
|
-
this.error = {message:
|
|
177
|
-
return cb(
|
|
212
|
+
this.error = {message: 'Invalid MIME type: ' + file.mimetype};
|
|
213
|
+
return cb(new Error('Invalid MIME type: ' + file.mimetype));
|
|
178
214
|
}
|
|
179
|
-
return cb(
|
|
215
|
+
return cb();
|
|
180
216
|
}
|
|
181
217
|
|
|
182
218
|
async validateFileContents(file, allowedFileType)
|
|
183
219
|
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
220
|
+
if(!FileHandler.isFile(file.path)){
|
|
221
|
+
this.error = {message: 'File path must be provided.', file};
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
let detectedType = FileHandler.detectFileType(file.path);
|
|
225
|
+
if(detectedType && 'application/octet-stream' !== detectedType){
|
|
226
|
+
let expectedMimeTypes = this.mimeTypes[allowedFileType] || [];
|
|
227
|
+
if(0 < expectedMimeTypes.length && -1 === expectedMimeTypes.indexOf(detectedType)){
|
|
228
|
+
this.error = {
|
|
229
|
+
message: 'File content type mismatch.',
|
|
230
|
+
detected: detectedType, expected: expectedMimeTypes
|
|
231
|
+
};
|
|
187
232
|
return false;
|
|
188
233
|
}
|
|
189
|
-
return FileHandler.validateFileType(file.path, allowedFileType, this.allowedExtensions, this.maxFileSize);
|
|
190
|
-
} catch(error){
|
|
191
|
-
this.error = {message: 'Error validating file contents.', error};
|
|
192
|
-
return false;
|
|
193
234
|
}
|
|
235
|
+
return FileHandler.validateFileType(file.path, allowedFileType, this.allowedExtensions, this.maxFileSize);
|
|
194
236
|
}
|
|
195
237
|
|
|
196
238
|
convertToRegex(key)
|
|
@@ -211,13 +253,7 @@ class UploaderFactory
|
|
|
211
253
|
}
|
|
212
254
|
for(let fieldName in files){
|
|
213
255
|
for(let file of files[fieldName]){
|
|
214
|
-
|
|
215
|
-
if(FileHandler.exists(file.path)){
|
|
216
|
-
FileHandler.remove(file.path);
|
|
217
|
-
}
|
|
218
|
-
} catch(error){
|
|
219
|
-
this.error = {message: 'Error cleaning up file.', error};
|
|
220
|
-
}
|
|
256
|
+
FileHandler.remove(file.path);
|
|
221
257
|
}
|
|
222
258
|
}
|
|
223
259
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reldens/server-utils",
|
|
3
3
|
"scope": "@reldens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.0",
|
|
5
5
|
"description": "Reldens - Server Utils",
|
|
6
6
|
"author": "Damian A. Pastorini",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"express-rate-limit": "7.5.0",
|
|
42
42
|
"express-session": "1.18.1",
|
|
43
43
|
"helmet": "8.1.0",
|
|
44
|
-
"multer": "
|
|
45
|
-
"sanitize-html": "^2.
|
|
44
|
+
"multer": "2.0.0",
|
|
45
|
+
"sanitize-html": "^2.17.0"
|
|
46
46
|
}
|
|
47
47
|
}
|