mastercontroller 1.2.12 → 1.2.13

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.
@@ -0,0 +1,546 @@
1
+ // version 1.0.0
2
+ // MasterController Input Validator - SQL Injection, Path Traversal, Command Injection Protection
3
+
4
+ /**
5
+ * Comprehensive input validation to prevent:
6
+ * - SQL Injection
7
+ * - NoSQL Injection
8
+ * - Path Traversal
9
+ * - Command Injection
10
+ * - LDAP Injection
11
+ * - XML Injection
12
+ * - Header Injection
13
+ */
14
+
15
+ const { logger } = require('./MasterErrorLogger');
16
+ const { escapeHTML } = require('./MasterSanitizer');
17
+ const path = require('path');
18
+
19
+ // SQL injection patterns
20
+ const SQL_INJECTION_PATTERNS = [
21
+ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|DECLARE)\b)/gi,
22
+ /(--|;|\/\*|\*\/|xp_|sp_)/gi,
23
+ /('|(\\'))|("|(\\"))(\s|$)/gi,
24
+ /(\bOR\b|\bAND\b).*?=.*?/gi,
25
+ /(0x[0-9a-f]+)/gi
26
+ ];
27
+
28
+ // NoSQL injection patterns (MongoDB, etc.)
29
+ const NOSQL_INJECTION_PATTERNS = [
30
+ /\$where/gi,
31
+ /\$ne/gi,
32
+ /\$gt/gi,
33
+ /\$lt/gi,
34
+ /\$regex/gi,
35
+ /\$nin/gi,
36
+ /\$in/gi
37
+ ];
38
+
39
+ // Command injection patterns
40
+ const COMMAND_INJECTION_PATTERNS = [
41
+ /[;&|`$()]/g,
42
+ /\n/g,
43
+ /\r/g
44
+ ];
45
+
46
+ // Path traversal patterns
47
+ const PATH_TRAVERSAL_PATTERNS = [
48
+ /\.\./g,
49
+ /\.\/\./g,
50
+ /%2e%2e/gi,
51
+ /%252e/gi,
52
+ /\.\.%2f/gi,
53
+ /\.\.%5c/gi
54
+ ];
55
+
56
+ // LDAP injection patterns
57
+ const LDAP_INJECTION_PATTERNS = [
58
+ /[*()\\]/g,
59
+ /\x00/g
60
+ ];
61
+
62
+ // Email validation regex
63
+ const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
64
+
65
+ // URL validation regex
66
+ const URL_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
67
+
68
+ // UUID validation regex
69
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
70
+
71
+ class MasterValidator {
72
+ constructor(options = {}) {
73
+ this.throwOnError = options.throwOnError || false;
74
+ this.logViolations = options.logViolations !== false;
75
+ }
76
+
77
+ /**
78
+ * Validate and sanitize string input
79
+ */
80
+ validateString(input, options = {}) {
81
+ const {
82
+ minLength = 0,
83
+ maxLength = 10000,
84
+ allowEmpty = true,
85
+ trim = true,
86
+ pattern = null,
87
+ name = 'input'
88
+ } = options;
89
+
90
+ // Type check
91
+ if (typeof input !== 'string') {
92
+ return this._handleError('INVALID_TYPE', `${name} must be a string`, { input });
93
+ }
94
+
95
+ let sanitized = trim ? input.trim() : input;
96
+
97
+ // Empty check
98
+ if (!allowEmpty && sanitized.length === 0) {
99
+ return this._handleError('EMPTY_STRING', `${name} cannot be empty`, { input });
100
+ }
101
+
102
+ // Length check
103
+ if (sanitized.length < minLength) {
104
+ return this._handleError('TOO_SHORT', `${name} must be at least ${minLength} characters`, { input });
105
+ }
106
+
107
+ if (sanitized.length > maxLength) {
108
+ sanitized = sanitized.substring(0, maxLength);
109
+ this._logWarning('STRING_TRUNCATED', `${name} was truncated to ${maxLength} characters`);
110
+ }
111
+
112
+ // Pattern check
113
+ if (pattern && !pattern.test(sanitized)) {
114
+ return this._handleError('PATTERN_MISMATCH', `${name} does not match required pattern`, { input });
115
+ }
116
+
117
+ return { valid: true, value: sanitized };
118
+ }
119
+
120
+ /**
121
+ * Validate and sanitize integer
122
+ */
123
+ validateInteger(input, options = {}) {
124
+ const {
125
+ min = Number.MIN_SAFE_INTEGER,
126
+ max = Number.MAX_SAFE_INTEGER,
127
+ name = 'input'
128
+ } = options;
129
+
130
+ const parsed = parseInt(input, 10);
131
+
132
+ if (isNaN(parsed)) {
133
+ return this._handleError('INVALID_INTEGER', `${name} must be a valid integer`, { input });
134
+ }
135
+
136
+ if (parsed < min) {
137
+ return this._handleError('TOO_SMALL', `${name} must be at least ${min}`, { input });
138
+ }
139
+
140
+ if (parsed > max) {
141
+ return this._handleError('TOO_LARGE', `${name} must be at most ${max}`, { input });
142
+ }
143
+
144
+ return { valid: true, value: parsed };
145
+ }
146
+
147
+ /**
148
+ * Validate email address
149
+ */
150
+ validateEmail(input, options = {}) {
151
+ const { name = 'email' } = options;
152
+
153
+ if (typeof input !== 'string') {
154
+ return this._handleError('INVALID_TYPE', `${name} must be a string`, { input });
155
+ }
156
+
157
+ const trimmed = input.trim().toLowerCase();
158
+
159
+ if (!EMAIL_REGEX.test(trimmed)) {
160
+ return this._handleError('INVALID_EMAIL', `${name} is not a valid email address`, { input });
161
+ }
162
+
163
+ return { valid: true, value: trimmed };
164
+ }
165
+
166
+ /**
167
+ * Validate URL
168
+ */
169
+ validateURL(input, options = {}) {
170
+ const { name = 'url', allowedProtocols = ['http:', 'https:'] } = options;
171
+
172
+ if (typeof input !== 'string') {
173
+ return this._handleError('INVALID_TYPE', `${name} must be a string`, { input });
174
+ }
175
+
176
+ const trimmed = input.trim();
177
+
178
+ if (!URL_REGEX.test(trimmed)) {
179
+ return this._handleError('INVALID_URL', `${name} is not a valid URL`, { input });
180
+ }
181
+
182
+ // Check protocol
183
+ try {
184
+ const url = new URL(trimmed);
185
+ if (!allowedProtocols.includes(url.protocol)) {
186
+ return this._handleError('INVALID_PROTOCOL', `${name} protocol must be ${allowedProtocols.join(' or ')}`, { input });
187
+ }
188
+ } catch (e) {
189
+ return this._handleError('INVALID_URL', `${name} is not a valid URL`, { input });
190
+ }
191
+
192
+ return { valid: true, value: trimmed };
193
+ }
194
+
195
+ /**
196
+ * Validate UUID
197
+ */
198
+ validateUUID(input, options = {}) {
199
+ const { name = 'uuid' } = options;
200
+
201
+ if (typeof input !== 'string') {
202
+ return this._handleError('INVALID_TYPE', `${name} must be a string`, { input });
203
+ }
204
+
205
+ if (!UUID_REGEX.test(input.trim())) {
206
+ return this._handleError('INVALID_UUID', `${name} is not a valid UUID`, { input });
207
+ }
208
+
209
+ return { valid: true, value: input.trim() };
210
+ }
211
+
212
+ /**
213
+ * Check for SQL injection attempts
214
+ */
215
+ detectSQLInjection(input, options = {}) {
216
+ if (typeof input !== 'string') {
217
+ return { safe: true, value: input };
218
+ }
219
+
220
+ for (const pattern of SQL_INJECTION_PATTERNS) {
221
+ if (pattern.test(input)) {
222
+ this._logViolation('SQL_INJECTION_ATTEMPT', input, pattern);
223
+ return { safe: false, threat: 'SQL_INJECTION', pattern: pattern.toString() };
224
+ }
225
+ }
226
+
227
+ return { safe: true, value: input };
228
+ }
229
+
230
+ /**
231
+ * Check for NoSQL injection attempts
232
+ */
233
+ detectNoSQLInjection(input) {
234
+ if (typeof input === 'object' && input !== null) {
235
+ const json = JSON.stringify(input);
236
+ for (const pattern of NOSQL_INJECTION_PATTERNS) {
237
+ if (pattern.test(json)) {
238
+ this._logViolation('NOSQL_INJECTION_ATTEMPT', json, pattern);
239
+ return { safe: false, threat: 'NOSQL_INJECTION', pattern: pattern.toString() };
240
+ }
241
+ }
242
+ }
243
+
244
+ return { safe: true, value: input };
245
+ }
246
+
247
+ /**
248
+ * Check for command injection attempts
249
+ */
250
+ detectCommandInjection(input, options = {}) {
251
+ if (typeof input !== 'string') {
252
+ return { safe: true, value: input };
253
+ }
254
+
255
+ for (const pattern of COMMAND_INJECTION_PATTERNS) {
256
+ if (pattern.test(input)) {
257
+ this._logViolation('COMMAND_INJECTION_ATTEMPT', input, pattern);
258
+ return { safe: false, threat: 'COMMAND_INJECTION', pattern: pattern.toString() };
259
+ }
260
+ }
261
+
262
+ return { safe: true, value: input };
263
+ }
264
+
265
+ /**
266
+ * Check for path traversal attempts
267
+ */
268
+ detectPathTraversal(input, options = {}) {
269
+ if (typeof input !== 'string') {
270
+ return { safe: true, value: input };
271
+ }
272
+
273
+ for (const pattern of PATH_TRAVERSAL_PATTERNS) {
274
+ if (pattern.test(input)) {
275
+ this._logViolation('PATH_TRAVERSAL_ATTEMPT', input, pattern);
276
+ return { safe: false, threat: 'PATH_TRAVERSAL', pattern: pattern.toString() };
277
+ }
278
+ }
279
+
280
+ return { safe: true, value: input };
281
+ }
282
+
283
+ /**
284
+ * Sanitize file path to prevent path traversal
285
+ */
286
+ sanitizeFilePath(input, options = {}) {
287
+ const { basePath = null, name = 'path' } = options;
288
+
289
+ if (typeof input !== 'string') {
290
+ return this._handleError('INVALID_TYPE', `${name} must be a string`, { input });
291
+ }
292
+
293
+ // Check for path traversal
294
+ const traversalCheck = this.detectPathTraversal(input);
295
+ if (!traversalCheck.safe) {
296
+ return this._handleError('PATH_TRAVERSAL', `${name} contains path traversal attempt`, { input });
297
+ }
298
+
299
+ // Normalize path
300
+ const normalized = path.normalize(input);
301
+
302
+ // If basePath provided, ensure path is within it
303
+ if (basePath) {
304
+ const resolved = path.resolve(basePath, normalized);
305
+ const base = path.resolve(basePath);
306
+
307
+ if (!resolved.startsWith(base)) {
308
+ return this._handleError('PATH_OUTSIDE_BASE', `${name} is outside allowed directory`, { input, basePath });
309
+ }
310
+
311
+ return { valid: true, value: resolved };
312
+ }
313
+
314
+ return { valid: true, value: normalized };
315
+ }
316
+
317
+ /**
318
+ * Sanitize SQL input (use parameterized queries instead when possible)
319
+ */
320
+ sanitizeSQL(input) {
321
+ if (typeof input !== 'string') {
322
+ return input;
323
+ }
324
+
325
+ // Escape single quotes
326
+ let sanitized = input.replace(/'/g, "''");
327
+
328
+ // Remove SQL comments
329
+ sanitized = sanitized.replace(/--.*$/gm, '');
330
+ sanitized = sanitized.replace(/\/\*.*?\*\//gs, '');
331
+
332
+ // Check for injection after sanitization
333
+ const check = this.detectSQLInjection(sanitized);
334
+ if (!check.safe) {
335
+ this._logWarning('SQL_INJECTION_AFTER_SANITIZATION', 'Input still contains SQL patterns after sanitization');
336
+ return ''; // Return empty string if still dangerous
337
+ }
338
+
339
+ return sanitized;
340
+ }
341
+
342
+ /**
343
+ * Validate route parameters
344
+ */
345
+ validateRouteParams(params, schema = {}) {
346
+ const sanitized = {};
347
+ const errors = [];
348
+
349
+ for (const [key, value] of Object.entries(params)) {
350
+ const rules = schema[key];
351
+
352
+ if (!rules) {
353
+ // No validation rules, sanitize as string
354
+ sanitized[key] = escapeHTML(String(value));
355
+ continue;
356
+ }
357
+
358
+ let result;
359
+
360
+ switch (rules.type) {
361
+ case 'string':
362
+ result = this.validateString(value, rules);
363
+ break;
364
+ case 'integer':
365
+ result = this.validateInteger(value, rules);
366
+ break;
367
+ case 'email':
368
+ result = this.validateEmail(value, rules);
369
+ break;
370
+ case 'url':
371
+ result = this.validateURL(value, rules);
372
+ break;
373
+ case 'uuid':
374
+ result = this.validateUUID(value, rules);
375
+ break;
376
+ default:
377
+ result = { valid: true, value: escapeHTML(String(value)) };
378
+ }
379
+
380
+ if (result.valid) {
381
+ sanitized[key] = result.value;
382
+ } else {
383
+ errors.push({ param: key, error: result.error });
384
+ }
385
+ }
386
+
387
+ return {
388
+ valid: errors.length === 0,
389
+ params: sanitized,
390
+ errors
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Validate request body
396
+ */
397
+ validateRequestBody(body, schema = {}) {
398
+ return this.validateRouteParams(body, schema);
399
+ }
400
+
401
+ /**
402
+ * Sanitize object recursively
403
+ */
404
+ sanitizeObject(obj) {
405
+ if (obj === null || obj === undefined) {
406
+ return obj;
407
+ }
408
+
409
+ if (Array.isArray(obj)) {
410
+ return obj.map(item => this.sanitizeObject(item));
411
+ }
412
+
413
+ if (typeof obj === 'object') {
414
+ const sanitized = {};
415
+ for (const [key, value] of Object.entries(obj)) {
416
+ // Skip internal properties
417
+ if (key.startsWith('_') || key.startsWith('__')) {
418
+ continue;
419
+ }
420
+
421
+ sanitized[key] = this.sanitizeObject(value);
422
+ }
423
+ return sanitized;
424
+ }
425
+
426
+ if (typeof obj === 'string') {
427
+ return escapeHTML(obj);
428
+ }
429
+
430
+ return obj;
431
+ }
432
+
433
+ /**
434
+ * Handle validation error
435
+ */
436
+ _handleError(code, message, context = {}) {
437
+ const error = {
438
+ valid: false,
439
+ error: { code, message, context }
440
+ };
441
+
442
+ if (this.logViolations) {
443
+ logger.warn({
444
+ code: `MC_VALIDATION_${code}`,
445
+ message: message,
446
+ ...context
447
+ });
448
+ }
449
+
450
+ if (this.throwOnError) {
451
+ throw new Error(message);
452
+ }
453
+
454
+ return error;
455
+ }
456
+
457
+ /**
458
+ * Log validation warning
459
+ */
460
+ _logWarning(code, message, context = {}) {
461
+ if (this.logViolations) {
462
+ logger.warn({
463
+ code: `MC_VALIDATION_${code}`,
464
+ message: message,
465
+ ...context
466
+ });
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Log security violation
472
+ */
473
+ _logViolation(type, input, pattern) {
474
+ if (this.logViolations) {
475
+ logger.error({
476
+ code: `MC_SECURITY_${type}`,
477
+ message: `Security violation detected: ${type}`,
478
+ input: input.substring(0, 100), // Log first 100 chars only
479
+ pattern: pattern.toString(),
480
+ timestamp: new Date().toISOString()
481
+ });
482
+ }
483
+ }
484
+ }
485
+
486
+ // Create singleton instance
487
+ const validator = new MasterValidator();
488
+
489
+ /**
490
+ * Quick validation functions
491
+ */
492
+
493
+ function validateString(input, options) {
494
+ return validator.validateString(input, options);
495
+ }
496
+
497
+ function validateInteger(input, options) {
498
+ return validator.validateInteger(input, options);
499
+ }
500
+
501
+ function validateEmail(input, options) {
502
+ return validator.validateEmail(input, options);
503
+ }
504
+
505
+ function validateURL(input, options) {
506
+ return validator.validateURL(input, options);
507
+ }
508
+
509
+ function sanitizeSQL(input) {
510
+ return validator.sanitizeSQL(input);
511
+ }
512
+
513
+ function sanitizeFilePath(input, options) {
514
+ return validator.sanitizeFilePath(input, options);
515
+ }
516
+
517
+ function validateRouteParams(params, schema) {
518
+ return validator.validateRouteParams(params, schema);
519
+ }
520
+
521
+ function detectSQLInjection(input) {
522
+ return validator.detectSQLInjection(input);
523
+ }
524
+
525
+ function detectPathTraversal(input) {
526
+ return validator.detectPathTraversal(input);
527
+ }
528
+
529
+ function detectCommandInjection(input) {
530
+ return validator.detectCommandInjection(input);
531
+ }
532
+
533
+ module.exports = {
534
+ MasterValidator,
535
+ validator,
536
+ validateString,
537
+ validateInteger,
538
+ validateEmail,
539
+ validateURL,
540
+ sanitizeSQL,
541
+ sanitizeFilePath,
542
+ validateRouteParams,
543
+ detectSQLInjection,
544
+ detectPathTraversal,
545
+ detectCommandInjection
546
+ };
package/README.md CHANGED
@@ -49,50 +49,6 @@ Use `setupServer('https', credentials)` or configure via environment TLS; see do
49
49
  - `docs/server-setup-nginx-reverse-proxy.md`
50
50
  - `docs/environment-tls-reference.md`
51
51
 
52
- ### How File Uploads Work
53
-
54
- MasterController handles file uploads through the `formidable` library (v3.5.4+) integrated into the request parsing pipeline in `MasterRequest.js`.
55
-
56
- **Processing Flow:**
57
-
58
- 1. **Content-Type Detection** - When a request arrives, the framework parses the `Content-Type` header to determine how to handle the request body (`MasterRequest.js:34-36`)
59
-
60
- 2. **Multipart Form Data** - For `multipart/form-data` requests (file uploads), the framework uses formidable's `IncomingForm` to parse the request (`MasterRequest.js:43-78`)
61
-
62
- 3. **Event-Based Parsing** - Formidable emits events during parsing:
63
- - `field` event: Captures regular form fields and adds them to `parsedURL.formData.fields`
64
- - `file` event: Captures uploaded files and stores them in `parsedURL.formData.files` as arrays (supporting multiple file uploads per field)
65
- - `end` event: Signals completion and resolves the promise with parsed data
66
-
67
- 4. **File Metadata** - Each uploaded file object includes:
68
- - `name` or `originalFilename`: The original filename
69
- - `extension`: Extracted file extension (e.g., `.jpg`, `.pdf`)
70
- - `filepath`: Temporary location where formidable stored the file
71
- - Other formidable metadata (size, mimetype, etc.)
72
-
73
- 5. **Accessing Uploads in Controllers** - In your controller actions, access uploaded files via:
74
- ```js
75
- this.params.formData.files['fieldName'][0] // First file for 'fieldName'
76
- this.params.formData.fields['textField'] // Regular form fields
77
- ```
78
-
79
- 6. **Multiple Files** - Files are always stored as arrays in `parsedURL.formData.files[field]`, allowing multiple files to be uploaded with the same field name (`MasterRequest.js:59-65`)
80
-
81
- 7. **Cleanup** - Use `this.request.deleteFileBuffer(filePath)` to remove temporary files after processing (`MasterRequest.js:162-169`)
82
-
83
- **Configuration Options:**
84
-
85
- You can configure file upload behavior via `master.request.init()`:
86
- - `disableFormidableMultipartFormData`: Set to `true` to skip file upload parsing
87
- - `formidable`: Pass options directly to formidable (upload directory, max file size, etc.)
88
-
89
- **Supported Content Types:**
90
- - `multipart/form-data` - File uploads
91
- - `application/x-www-form-urlencoded` - Standard forms
92
- - `application/json` - JSON payloads
93
- - `text/plain` - Plain text (1MB limit)
94
- - `text/html` - HTML content
95
-
96
52
  ### Production tips
97
53
  - Prefer a reverse proxy for TLS and serve Node on a high port.
98
54
  - If keeping TLS in Node, harden TLS and manage cert rotation.