mastercontroller 1.2.11 → 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.
- package/CSPConfig.js +319 -0
- package/EventHandlerValidator.js +464 -0
- package/MasterAction.js +296 -72
- package/MasterBackendErrorHandler.js +769 -0
- package/MasterBenchmark.js +89 -0
- package/MasterBuildOptimizer.js +376 -0
- package/MasterBundleAnalyzer.js +108 -0
- package/MasterCache.js +400 -0
- package/MasterControl.js +77 -7
- package/MasterErrorHandler.js +487 -0
- package/MasterErrorLogger.js +360 -0
- package/MasterErrorMiddleware.js +407 -0
- package/MasterHtml.js +101 -14
- package/MasterMemoryMonitor.js +188 -0
- package/MasterProfiler.js +409 -0
- package/MasterRouter.js +273 -66
- package/MasterSanitizer.js +429 -0
- package/MasterTemplate.js +96 -3
- package/MasterValidator.js +546 -0
- package/README.md +0 -44
- package/SecurityMiddleware.js +486 -0
- package/SessionSecurity.js +416 -0
- package/package.json +2 -2
- package/ssr/ErrorBoundary.js +353 -0
- package/ssr/HTMLUtils.js +15 -0
- package/ssr/HydrationMismatch.js +265 -0
- package/ssr/PerformanceMonitor.js +233 -0
- package/ssr/SSRErrorHandler.js +273 -0
- package/ssr/hydration-client.js +93 -0
- package/ssr/runtime-ssr.cjs +553 -0
- package/ssr/ssr-shims.js +73 -0
- package/examples/FileServingExample.js +0 -88
|
@@ -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.
|