mastercontroller 1.2.12 → 1.2.14
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/.claude/settings.local.json +12 -0
- package/MasterAction.js +297 -73
- package/MasterControl.js +112 -19
- package/MasterHtml.js +101 -14
- package/MasterRouter.js +281 -66
- package/MasterTemplate.js +96 -3
- package/README.md +0 -44
- package/error/ErrorBoundary.js +353 -0
- package/error/HydrationMismatch.js +265 -0
- package/error/MasterBackendErrorHandler.js +769 -0
- package/{MasterError.js → error/MasterError.js} +2 -2
- package/error/MasterErrorHandler.js +487 -0
- package/error/MasterErrorLogger.js +360 -0
- package/error/MasterErrorMiddleware.js +407 -0
- package/error/SSRErrorHandler.js +273 -0
- package/monitoring/MasterCache.js +400 -0
- package/monitoring/MasterMemoryMonitor.js +188 -0
- package/monitoring/MasterProfiler.js +409 -0
- package/monitoring/PerformanceMonitor.js +233 -0
- package/package.json +3 -3
- package/security/CSPConfig.js +319 -0
- package/security/EventHandlerValidator.js +464 -0
- package/security/MasterSanitizer.js +429 -0
- package/security/MasterValidator.js +546 -0
- package/security/SecurityMiddleware.js +486 -0
- package/security/SessionSecurity.js +416 -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,464 @@
|
|
|
1
|
+
// version 1.0.1
|
|
2
|
+
// MasterController Event Handler Validator - Prevent code injection in @event attributes
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates @event attribute expressions to prevent:
|
|
6
|
+
* - Arbitrary code execution
|
|
7
|
+
* - XSS through event handlers
|
|
8
|
+
* - Malicious function calls
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { logger } = require('../error/MasterErrorLogger');
|
|
12
|
+
const { MasterControllerError } = require('../error/MasterErrorHandler');
|
|
13
|
+
|
|
14
|
+
// Valid patterns for event handler expressions
|
|
15
|
+
const VALID_PATTERNS = [
|
|
16
|
+
// this.methodName
|
|
17
|
+
/^this\.[a-zA-Z_$][a-zA-Z0-9_$]*$/,
|
|
18
|
+
|
|
19
|
+
// this.methodName()
|
|
20
|
+
/^this\.[a-zA-Z_$][a-zA-Z0-9_$]*\(\)$/,
|
|
21
|
+
|
|
22
|
+
// this.methodName(arg1, arg2)
|
|
23
|
+
/^this\.[a-zA-Z_$][a-zA-Z0-9_$]*\([^)]*\)$/,
|
|
24
|
+
|
|
25
|
+
// component.methodName (for child components)
|
|
26
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*\.[a-zA-Z_$][a-zA-Z0-9_$]*$/,
|
|
27
|
+
|
|
28
|
+
// component.methodName()
|
|
29
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*\.[a-zA-Z_$][a-zA-Z0-9_$]*\(\)$/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Dangerous patterns that should be blocked
|
|
33
|
+
const DANGEROUS_PATTERNS = [
|
|
34
|
+
// eval, Function constructor
|
|
35
|
+
/\beval\s*\(/i,
|
|
36
|
+
/new\s+Function\s*\(/i,
|
|
37
|
+
|
|
38
|
+
// setTimeout, setInterval with string
|
|
39
|
+
/setTimeout\s*\(\s*["'`]/i,
|
|
40
|
+
/setInterval\s*\(\s*["'`]/i,
|
|
41
|
+
|
|
42
|
+
// Script injection
|
|
43
|
+
/<script/i,
|
|
44
|
+
/javascript:/i,
|
|
45
|
+
/on\w+\s*=/i, // onclick=, onerror=, etc.
|
|
46
|
+
|
|
47
|
+
// Document/window manipulation
|
|
48
|
+
/document\.\w+\s*=/i,
|
|
49
|
+
/window\.\w+\s*=/i,
|
|
50
|
+
/location\s*=/i,
|
|
51
|
+
|
|
52
|
+
// Code execution
|
|
53
|
+
/\.constructor\s*\(/i,
|
|
54
|
+
/__proto__/i,
|
|
55
|
+
/prototype/i,
|
|
56
|
+
|
|
57
|
+
// Dangerous characters
|
|
58
|
+
/[;&|`$]/,
|
|
59
|
+
|
|
60
|
+
// Import/require
|
|
61
|
+
/\bimport\s*\(/i,
|
|
62
|
+
/\brequire\s*\(/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Whitelist of safe built-in methods
|
|
66
|
+
const SAFE_METHODS = [
|
|
67
|
+
'preventDefault',
|
|
68
|
+
'stopPropagation',
|
|
69
|
+
'stopImmediatePropagation',
|
|
70
|
+
'log',
|
|
71
|
+
'warn',
|
|
72
|
+
'error'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
class EventHandlerValidator {
|
|
76
|
+
constructor(options = {}) {
|
|
77
|
+
this.strict = options.strict !== false;
|
|
78
|
+
this.throwOnError = options.throwOnError || false;
|
|
79
|
+
this.logViolations = options.logViolations !== false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate event handler expression
|
|
84
|
+
*/
|
|
85
|
+
validateHandler(expression, context = {}) {
|
|
86
|
+
if (!expression || typeof expression !== 'string') {
|
|
87
|
+
return this._handleError(
|
|
88
|
+
'INVALID_EXPRESSION',
|
|
89
|
+
'Event handler expression must be a non-empty string',
|
|
90
|
+
{ expression, context }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const trimmed = expression.trim();
|
|
95
|
+
|
|
96
|
+
// Check for empty expression
|
|
97
|
+
if (trimmed.length === 0) {
|
|
98
|
+
return this._handleError(
|
|
99
|
+
'EMPTY_EXPRESSION',
|
|
100
|
+
'Event handler expression cannot be empty',
|
|
101
|
+
{ expression, context }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for dangerous patterns first
|
|
106
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
107
|
+
if (pattern.test(trimmed)) {
|
|
108
|
+
return this._handleError(
|
|
109
|
+
'DANGEROUS_PATTERN',
|
|
110
|
+
`Event handler contains dangerous pattern: ${pattern.toString()}`,
|
|
111
|
+
{ expression: trimmed, pattern: pattern.toString(), context }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if expression matches valid patterns
|
|
117
|
+
const isValid = VALID_PATTERNS.some(pattern => pattern.test(trimmed));
|
|
118
|
+
|
|
119
|
+
if (!isValid && this.strict) {
|
|
120
|
+
return this._handleError(
|
|
121
|
+
'INVALID_PATTERN',
|
|
122
|
+
'Event handler expression does not match allowed patterns',
|
|
123
|
+
{ expression: trimmed, context }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Additional validation for specific patterns
|
|
128
|
+
const validation = this._validateSpecificPattern(trimmed, context);
|
|
129
|
+
if (!validation.valid) {
|
|
130
|
+
return validation;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { valid: true, expression: trimmed };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate specific pattern details
|
|
138
|
+
*/
|
|
139
|
+
_validateSpecificPattern(expression, context) {
|
|
140
|
+
// Check for this.methodName pattern
|
|
141
|
+
if (expression.startsWith('this.')) {
|
|
142
|
+
const methodName = expression.substring(5).replace(/\(.*\)$/, '');
|
|
143
|
+
|
|
144
|
+
if (methodName.length === 0) {
|
|
145
|
+
return this._handleError(
|
|
146
|
+
'INVALID_METHOD',
|
|
147
|
+
'Method name cannot be empty',
|
|
148
|
+
{ expression, context }
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for reserved JavaScript keywords
|
|
153
|
+
if (this._isReservedKeyword(methodName)) {
|
|
154
|
+
return this._handleError(
|
|
155
|
+
'RESERVED_KEYWORD',
|
|
156
|
+
`Cannot use reserved keyword as method name: ${methodName}`,
|
|
157
|
+
{ expression, methodName, context }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check for arguments if present
|
|
163
|
+
if (expression.includes('(') && expression.includes(')')) {
|
|
164
|
+
const argsMatch = expression.match(/\(([^)]*)\)/);
|
|
165
|
+
if (argsMatch && argsMatch[1].trim().length > 0) {
|
|
166
|
+
const args = argsMatch[1];
|
|
167
|
+
const argValidation = this._validateArguments(args, expression, context);
|
|
168
|
+
if (!argValidation.valid) {
|
|
169
|
+
return argValidation;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { valid: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate function arguments
|
|
179
|
+
*/
|
|
180
|
+
_validateArguments(argsString, expression, context) {
|
|
181
|
+
// Split by comma, but respect nested parentheses
|
|
182
|
+
const args = this._splitArguments(argsString);
|
|
183
|
+
|
|
184
|
+
for (const arg of args) {
|
|
185
|
+
const trimmedArg = arg.trim();
|
|
186
|
+
|
|
187
|
+
// Allow: numbers, strings, booleans, null, undefined, event, this, simple property access
|
|
188
|
+
const validArgPatterns = [
|
|
189
|
+
/^[0-9]+$/, // numbers
|
|
190
|
+
/^[0-9]+\.[0-9]+$/, // decimals
|
|
191
|
+
/^["'].*["']$/, // strings
|
|
192
|
+
/^`.*`$/, // template strings (limited)
|
|
193
|
+
/^true$/, // boolean true
|
|
194
|
+
/^false$/, // boolean false
|
|
195
|
+
/^null$/, // null
|
|
196
|
+
/^undefined$/, // undefined
|
|
197
|
+
/^event$/, // event object
|
|
198
|
+
/^this$/, // this reference
|
|
199
|
+
/^this\.[a-zA-Z_$][a-zA-Z0-9_$]*$/, // this.property
|
|
200
|
+
/^event\.[a-zA-Z_$][a-zA-Z0-9_$.]*$/, // event.property.nested
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const isValidArg = validArgPatterns.some(pattern => pattern.test(trimmedArg));
|
|
204
|
+
|
|
205
|
+
if (!isValidArg) {
|
|
206
|
+
return this._handleError(
|
|
207
|
+
'INVALID_ARGUMENT',
|
|
208
|
+
`Invalid argument in event handler: ${trimmedArg}`,
|
|
209
|
+
{ expression, argument: trimmedArg, context }
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for dangerous content in string arguments
|
|
214
|
+
if (/^["'`]/.test(trimmedArg)) {
|
|
215
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
216
|
+
if (pattern.test(trimmedArg)) {
|
|
217
|
+
return this._handleError(
|
|
218
|
+
'DANGEROUS_ARGUMENT',
|
|
219
|
+
`Dangerous content in argument: ${trimmedArg}`,
|
|
220
|
+
{ expression, argument: trimmedArg, context }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { valid: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Split arguments respecting nested structures
|
|
232
|
+
*/
|
|
233
|
+
_splitArguments(argsString) {
|
|
234
|
+
const args = [];
|
|
235
|
+
let current = '';
|
|
236
|
+
let depth = 0;
|
|
237
|
+
let inString = false;
|
|
238
|
+
let stringChar = '';
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
241
|
+
const char = argsString[i];
|
|
242
|
+
|
|
243
|
+
if (!inString) {
|
|
244
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
245
|
+
inString = true;
|
|
246
|
+
stringChar = char;
|
|
247
|
+
current += char;
|
|
248
|
+
} else if (char === '(' || char === '[' || char === '{') {
|
|
249
|
+
depth++;
|
|
250
|
+
current += char;
|
|
251
|
+
} else if (char === ')' || char === ']' || char === '}') {
|
|
252
|
+
depth--;
|
|
253
|
+
current += char;
|
|
254
|
+
} else if (char === ',' && depth === 0) {
|
|
255
|
+
args.push(current);
|
|
256
|
+
current = '';
|
|
257
|
+
} else {
|
|
258
|
+
current += char;
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
current += char;
|
|
262
|
+
if (char === stringChar && argsString[i - 1] !== '\\') {
|
|
263
|
+
inString = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (current.trim().length > 0) {
|
|
269
|
+
args.push(current);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return args;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if identifier is a reserved JavaScript keyword
|
|
277
|
+
*/
|
|
278
|
+
_isReservedKeyword(identifier) {
|
|
279
|
+
const reserved = [
|
|
280
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
281
|
+
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
|
|
282
|
+
'for', 'function', 'if', 'import', 'in', 'instanceof', 'new',
|
|
283
|
+
'return', 'super', 'switch', 'this', 'throw', 'try', 'typeof',
|
|
284
|
+
'var', 'void', 'while', 'with', 'yield'
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
return reserved.includes(identifier);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Validate @event attribute (attribute name + expression)
|
|
292
|
+
*/
|
|
293
|
+
validateEventAttribute(attrName, attrValue, context = {}) {
|
|
294
|
+
// Validate attribute name format
|
|
295
|
+
if (!attrName || !attrName.startsWith('@')) {
|
|
296
|
+
return this._handleError(
|
|
297
|
+
'INVALID_ATTRIBUTE_NAME',
|
|
298
|
+
'Event attribute must start with @',
|
|
299
|
+
{ attrName, attrValue, context }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract event type and component name
|
|
304
|
+
// Format: @eventType-componentName or @eventType
|
|
305
|
+
const eventName = attrName.substring(1); // Remove @
|
|
306
|
+
const parts = eventName.split('-');
|
|
307
|
+
|
|
308
|
+
if (parts.length < 1) {
|
|
309
|
+
return this._handleError(
|
|
310
|
+
'INVALID_EVENT_NAME',
|
|
311
|
+
'Event name cannot be empty',
|
|
312
|
+
{ attrName, attrValue, context }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Validate event type (first part)
|
|
317
|
+
const eventType = parts[0];
|
|
318
|
+
if (!/^[a-z][a-z0-9]*$/.test(eventType)) {
|
|
319
|
+
return this._handleError(
|
|
320
|
+
'INVALID_EVENT_TYPE',
|
|
321
|
+
'Event type must be lowercase alphanumeric',
|
|
322
|
+
{ attrName, eventType, attrValue, context }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Validate component name if present (second part)
|
|
327
|
+
if (parts.length > 1) {
|
|
328
|
+
const componentName = parts.slice(1).join('-');
|
|
329
|
+
if (!/^[a-z][a-z0-9-]*$/.test(componentName)) {
|
|
330
|
+
return this._handleError(
|
|
331
|
+
'INVALID_COMPONENT_NAME',
|
|
332
|
+
'Component name must be lowercase with hyphens',
|
|
333
|
+
{ attrName, componentName, attrValue, context }
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Validate handler expression
|
|
339
|
+
return this.validateHandler(attrValue, { ...context, attrName });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Sanitize event handler expression (remove dangerous content)
|
|
344
|
+
*/
|
|
345
|
+
sanitizeHandler(expression) {
|
|
346
|
+
if (!expression || typeof expression !== 'string') {
|
|
347
|
+
return '';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let sanitized = expression.trim();
|
|
351
|
+
|
|
352
|
+
// Remove dangerous content
|
|
353
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
354
|
+
sanitized = sanitized.replace(pattern, '');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Validate after sanitization
|
|
358
|
+
const validation = this.validateHandler(sanitized);
|
|
359
|
+
if (!validation.valid) {
|
|
360
|
+
if (this.logViolations) {
|
|
361
|
+
logger.warn({
|
|
362
|
+
code: 'MC_SECURITY_HANDLER_SANITIZED',
|
|
363
|
+
message: 'Event handler sanitized but still invalid',
|
|
364
|
+
original: expression,
|
|
365
|
+
sanitized: sanitized
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return ''; // Return empty string if still invalid
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return sanitized;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Handle validation error
|
|
376
|
+
*/
|
|
377
|
+
_handleError(code, message, context = {}) {
|
|
378
|
+
const error = {
|
|
379
|
+
valid: false,
|
|
380
|
+
error: { code, message, context }
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (this.logViolations) {
|
|
384
|
+
logger.error({
|
|
385
|
+
code: `MC_SECURITY_EVENT_${code}`,
|
|
386
|
+
message: message,
|
|
387
|
+
...context
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (this.throwOnError) {
|
|
392
|
+
throw new MasterControllerError({
|
|
393
|
+
code: `MC_SECURITY_EVENT_${code}`,
|
|
394
|
+
message: message,
|
|
395
|
+
...context
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Create singleton instance
|
|
404
|
+
const validator = new EventHandlerValidator();
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Quick validation functions
|
|
408
|
+
*/
|
|
409
|
+
|
|
410
|
+
function validateHandler(expression, context) {
|
|
411
|
+
return validator.validateHandler(expression, context);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateEventAttribute(attrName, attrValue, context) {
|
|
415
|
+
return validator.validateEventAttribute(attrName, attrValue, context);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function sanitizeHandler(expression) {
|
|
419
|
+
return validator.sanitizeHandler(expression);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Safe event handler wrapper for use in components
|
|
424
|
+
*/
|
|
425
|
+
function createSafeHandler(handler, component) {
|
|
426
|
+
return function safeEventHandler(event) {
|
|
427
|
+
try {
|
|
428
|
+
// Validate event object
|
|
429
|
+
if (!event || typeof event !== 'object') {
|
|
430
|
+
logger.warn({
|
|
431
|
+
code: 'MC_SECURITY_INVALID_EVENT',
|
|
432
|
+
message: 'Invalid event object passed to handler'
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Call handler with proper context
|
|
438
|
+
return handler.call(component, event);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
logger.error({
|
|
441
|
+
code: 'MC_SECURITY_HANDLER_ERROR',
|
|
442
|
+
message: 'Error in event handler',
|
|
443
|
+
error: error.message,
|
|
444
|
+
stack: error.stack
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Rethrow in development
|
|
448
|
+
if (process.env.NODE_ENV === 'development') {
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = {
|
|
456
|
+
EventHandlerValidator,
|
|
457
|
+
validator,
|
|
458
|
+
validateHandler,
|
|
459
|
+
validateEventAttribute,
|
|
460
|
+
sanitizeHandler,
|
|
461
|
+
createSafeHandler,
|
|
462
|
+
VALID_PATTERNS,
|
|
463
|
+
DANGEROUS_PATTERNS
|
|
464
|
+
};
|