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,464 @@
1
+ // version 1.0.0
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('./MasterErrorLogger');
12
+ const { MasterControllerError } = require('./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
+ };