mastercontroller 1.3.8 → 1.3.10
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 +4 -1
- package/FIXES_APPLIED.md +378 -0
- package/MasterAction.js +10 -263
- package/MasterControl.js +128 -27
- package/MasterRequest.js +6 -0
- package/MasterRouter.js +27 -32
- package/PERFORMANCE_SECURITY_AUDIT.md +677 -0
- package/README.md +117 -43
- package/monitoring/README.md +3112 -0
- package/package.json +1 -1
- package/security/README.md +1805 -0
- package/test-raw-body-preservation.js +128 -0
- package/MasterCors.js.tmp +0 -0
- package/MasterHtml.js +0 -649
- package/MasterPipeline.js.tmp +0 -0
- package/MasterRequest.js.tmp +0 -0
- package/MasterRouter.js.tmp +0 -0
- package/MasterSocket.js.tmp +0 -0
- package/MasterTemp.js.tmp +0 -0
- package/MasterTemplate.js +0 -230
- package/MasterTimeout.js.tmp +0 -0
- package/TemplateOverwrite.js +0 -41
- package/TemplateOverwrite.js.tmp +0 -0
- package/ssr/hydration-client.js +0 -93
- package/ssr/runtime-ssr.cjs +0 -553
- package/ssr/ssr-shims.js +0 -73
package/MasterControl.js
CHANGED
|
@@ -67,6 +67,7 @@ class MasterControl {
|
|
|
67
67
|
_hstsMaxAge = 31536000 // 1 year default
|
|
68
68
|
_hstsIncludeSubDomains = true
|
|
69
69
|
_hstsPreload = false
|
|
70
|
+
_viewEngine = null // Pluggable view engine (MasterView, EJS, Pug, etc.)
|
|
70
71
|
|
|
71
72
|
#loadTransientListClasses(name, params){
|
|
72
73
|
Object.defineProperty(this.requestList, name, {
|
|
@@ -124,35 +125,127 @@ class MasterControl {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Initialize prototype pollution protection
|
|
130
|
+
* SECURITY: Prevents malicious modification of Object/Array prototypes
|
|
131
|
+
*/
|
|
132
|
+
_initPrototypePollutionProtection() {
|
|
133
|
+
// Only freeze in production to allow for easier debugging in development
|
|
134
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
135
|
+
|
|
136
|
+
if (isProduction) {
|
|
137
|
+
// Freeze prototypes to prevent prototype pollution attacks
|
|
138
|
+
try {
|
|
139
|
+
Object.freeze(Object.prototype);
|
|
140
|
+
Object.freeze(Array.prototype);
|
|
141
|
+
Object.freeze(Function.prototype);
|
|
142
|
+
|
|
143
|
+
logger.info({
|
|
144
|
+
code: 'MC_SECURITY_PROTOTYPE_FROZEN',
|
|
145
|
+
message: 'Prototypes frozen in production mode for security'
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger.warn({
|
|
149
|
+
code: 'MC_SECURITY_FREEZE_FAILED',
|
|
150
|
+
message: 'Failed to freeze prototypes',
|
|
151
|
+
error: err.message
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add prototype pollution detection utility
|
|
157
|
+
this._detectPrototypePollution = (obj) => {
|
|
158
|
+
if (!obj || typeof obj !== 'object') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
|
|
163
|
+
|
|
164
|
+
for (const key of dangerousKeys) {
|
|
165
|
+
if (key in obj) {
|
|
166
|
+
logger.error({
|
|
167
|
+
code: 'MC_SECURITY_PROTOTYPE_POLLUTION',
|
|
168
|
+
message: `Prototype pollution detected: ${key} in object`,
|
|
169
|
+
severity: 'CRITICAL'
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
console.log('[MasterControl] Prototype pollution protection initialized');
|
|
179
|
+
}
|
|
180
|
+
|
|
127
181
|
// extends class methods to be used inside of the view class using the THIS keyword
|
|
128
182
|
extendView( name, element){
|
|
129
183
|
element = new element();
|
|
130
|
-
|
|
131
|
-
var propertyNames = Object.getOwnPropertyNames( element.__proto__);
|
|
184
|
+
const propertyNames = Object.getOwnPropertyNames(element.__proto__);
|
|
132
185
|
this.viewList[name] = {};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
186
|
+
|
|
187
|
+
// Fixed: Use for...of instead of for...in for array iteration
|
|
188
|
+
// Filter out 'constructor' and iterate efficiently
|
|
189
|
+
for (const propName of propertyNames) {
|
|
190
|
+
if (propName !== "constructor") {
|
|
191
|
+
this.viewList[name][propName] = element[propName];
|
|
138
192
|
}
|
|
139
|
-
}
|
|
193
|
+
}
|
|
140
194
|
}
|
|
141
195
|
|
|
142
196
|
// extends class methods to be used inside of the controller class using the THIS keyword
|
|
143
197
|
extendController(element){
|
|
144
198
|
element = new element();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
199
|
+
const propertyNames = Object.getOwnPropertyNames(element.__proto__);
|
|
200
|
+
|
|
201
|
+
// Fixed: Use for...of instead of for...in for array iteration
|
|
202
|
+
// Filter out 'constructor' and iterate efficiently
|
|
203
|
+
for (const propName of propertyNames) {
|
|
204
|
+
if (propName !== "constructor") {
|
|
205
|
+
this.controllerList[propName] = element[propName];
|
|
152
206
|
}
|
|
153
|
-
}
|
|
207
|
+
}
|
|
154
208
|
}
|
|
155
|
-
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Register a view engine (MasterView, React, EJS, Pug, etc.)
|
|
212
|
+
* This allows for pluggable view rendering
|
|
213
|
+
*
|
|
214
|
+
* @param {Object|Function} ViewEngine - View engine class or instance
|
|
215
|
+
* @param {Object} options - Configuration options for the view engine
|
|
216
|
+
* @returns {MasterControl} - Returns this for chaining
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* // Use MasterView (official view engine)
|
|
220
|
+
* const MasterView = require('masterview');
|
|
221
|
+
* master.useView(MasterView, { ssr: true });
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // Use EJS adapter
|
|
225
|
+
* const EJSAdapter = require('./adapters/ejs');
|
|
226
|
+
* master.useView(EJSAdapter);
|
|
227
|
+
*/
|
|
228
|
+
useView(ViewEngine, options = {}) {
|
|
229
|
+
if (typeof ViewEngine === 'function') {
|
|
230
|
+
this._viewEngine = new ViewEngine(options);
|
|
231
|
+
} else {
|
|
232
|
+
this._viewEngine = ViewEngine;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Let the view engine register itself
|
|
236
|
+
if (this._viewEngine && this._viewEngine.register) {
|
|
237
|
+
this._viewEngine.register(this);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.info({
|
|
241
|
+
code: 'MC_INFO_VIEW_ENGINE_REGISTERED',
|
|
242
|
+
message: 'View engine registered',
|
|
243
|
+
engine: ViewEngine.name || 'Custom'
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
|
|
156
249
|
/*
|
|
157
250
|
Services are created each time they are requested.
|
|
158
251
|
It gets a new instance of the injected object, on each request of this object.
|
|
@@ -299,6 +392,9 @@ class MasterControl {
|
|
|
299
392
|
try {
|
|
300
393
|
var $that = this;
|
|
301
394
|
|
|
395
|
+
// SECURITY: Initialize prototype pollution protection
|
|
396
|
+
this._initPrototypePollutionProtection();
|
|
397
|
+
|
|
302
398
|
// AUTO-LOAD internal framework modules
|
|
303
399
|
// These are required for the framework to function and are loaded transparently
|
|
304
400
|
const internalModules = {
|
|
@@ -313,10 +409,11 @@ class MasterControl {
|
|
|
313
409
|
'MasterCors': './MasterCors',
|
|
314
410
|
'SessionSecurity': './security/SessionSecurity',
|
|
315
411
|
'MasterSocket': './MasterSocket',
|
|
316
|
-
'
|
|
317
|
-
|
|
318
|
-
'
|
|
319
|
-
'
|
|
412
|
+
'MasterTools': './MasterTools'
|
|
413
|
+
// View modules removed - use master.useView(MasterView) instead
|
|
414
|
+
// 'MasterHtml': './MasterHtml',
|
|
415
|
+
// 'MasterTemplate': './MasterTemplate',
|
|
416
|
+
// 'TemplateOverwrite': './TemplateOverwrite'
|
|
320
417
|
};
|
|
321
418
|
|
|
322
419
|
// Explicit module registration (prevents circular dependency issues)
|
|
@@ -331,8 +428,8 @@ class MasterControl {
|
|
|
331
428
|
'cors': { path: './MasterCors', exportName: 'MasterCors' },
|
|
332
429
|
'socket': { path: './MasterSocket', exportName: 'MasterSocket' },
|
|
333
430
|
'tempdata': { path: './MasterTemp', exportName: 'MasterTemp' },
|
|
334
|
-
'overwrite': { path: './TemplateOverwrite', exportName: 'TemplateOverwrite' },
|
|
335
431
|
'session': { path: './security/SessionSecurity', exportName: 'MasterSessionSecurity' }
|
|
432
|
+
// 'overwrite' removed - will be provided by view engine (master.useView())
|
|
336
433
|
};
|
|
337
434
|
|
|
338
435
|
for (const [name, config] of Object.entries(moduleRegistry)) {
|
|
@@ -354,13 +451,12 @@ class MasterControl {
|
|
|
354
451
|
// Legacy code uses master.sessions (plural), new API uses master.session (singular)
|
|
355
452
|
$that.sessions = $that.session;
|
|
356
453
|
|
|
357
|
-
// Load
|
|
454
|
+
// Load controller extensions (these extend prototypes, not master instance)
|
|
358
455
|
try {
|
|
359
456
|
require('./MasterAction');
|
|
360
457
|
require('./MasterActionFilters');
|
|
361
|
-
require('./MasterHtml');
|
|
362
|
-
require('./MasterTemplate');
|
|
363
458
|
require('./MasterTools');
|
|
459
|
+
// View extensions (MasterHtml, MasterTemplate) removed - use master.useView() instead
|
|
364
460
|
} catch (e) {
|
|
365
461
|
console.error('[MasterControl] Failed to load extensions:', e.message);
|
|
366
462
|
}
|
|
@@ -734,9 +830,14 @@ class MasterControl {
|
|
|
734
830
|
});
|
|
735
831
|
|
|
736
832
|
// 4. Load Scoped Services (per request - always needed)
|
|
833
|
+
// Cache keys for performance (computed once, not on every request)
|
|
834
|
+
const scopedKeys = Object.keys($that._scopedList);
|
|
835
|
+
|
|
737
836
|
$that.pipeline.use(async (ctx, next) => {
|
|
738
|
-
|
|
739
|
-
|
|
837
|
+
// Fixed: Use cached keys with direct array iteration (faster & safer)
|
|
838
|
+
for (let i = 0; i < scopedKeys.length; i++) {
|
|
839
|
+
const key = scopedKeys[i];
|
|
840
|
+
const className = $that._scopedList[key];
|
|
740
841
|
$that.requestList[key] = new className();
|
|
741
842
|
}
|
|
742
843
|
await next();
|
package/MasterRequest.js
CHANGED
|
@@ -290,6 +290,8 @@ class MasterRequest{
|
|
|
290
290
|
|
|
291
291
|
buffer += decoder.end();
|
|
292
292
|
var buff = qs.parse(buffer);
|
|
293
|
+
// Preserve raw body for signature verification
|
|
294
|
+
buff._rawBody = buffer;
|
|
293
295
|
func(buff);
|
|
294
296
|
});
|
|
295
297
|
|
|
@@ -343,6 +345,10 @@ class MasterRequest{
|
|
|
343
345
|
|
|
344
346
|
try {
|
|
345
347
|
var buff = JSON.parse(buffer);
|
|
348
|
+
// IMPORTANT: Preserve raw body for webhook signature verification
|
|
349
|
+
// Many webhook providers (Stripe, GitHub, Shopify, etc.) require the
|
|
350
|
+
// exact raw body string to verify HMAC signatures
|
|
351
|
+
buff._rawBody = buffer;
|
|
346
352
|
func(buff);
|
|
347
353
|
} catch (e) {
|
|
348
354
|
// Security: Don't fallback to qs.parse to avoid prototype pollution
|
package/MasterRouter.js
CHANGED
|
@@ -121,32 +121,33 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if(routeList.length > 0){
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// FIXED: Use for...of instead of for...in for array iteration
|
|
125
|
+
// This prevents prototype pollution and improves performance
|
|
126
|
+
for(const route of routeList){
|
|
126
127
|
// Store current route for error handling
|
|
127
128
|
currentRouteBeingProcessed = {
|
|
128
|
-
path:
|
|
129
|
-
toController:
|
|
130
|
-
toAction:
|
|
131
|
-
type:
|
|
129
|
+
path: route.path,
|
|
130
|
+
toController: route.toController,
|
|
131
|
+
toAction: route.toAction,
|
|
132
|
+
type: route.type
|
|
132
133
|
};
|
|
133
134
|
|
|
134
135
|
try {
|
|
135
|
-
requestObject.toController =
|
|
136
|
-
requestObject.toAction =
|
|
136
|
+
requestObject.toController = route.toController;
|
|
137
|
+
requestObject.toAction = route.toAction;
|
|
137
138
|
|
|
138
139
|
// FIX: Create a clean copy of params for each route test to prevent parameter pollution
|
|
139
140
|
// This prevents parameters from non-matching routes from accumulating in requestObject.params
|
|
140
141
|
var testParams = Object.assign({}, requestObject.params);
|
|
141
|
-
var pathObj = normalizePaths(requestObject.pathName,
|
|
142
|
+
var pathObj = normalizePaths(requestObject.pathName, route.path, testParams);
|
|
142
143
|
|
|
143
144
|
// if we find the route that matches the request
|
|
144
|
-
if(pathObj.requestPath === pathObj.routePath &&
|
|
145
|
+
if(pathObj.requestPath === pathObj.routePath && route.type === requestObject.type){
|
|
145
146
|
// Only commit the extracted params if this route actually matches
|
|
146
147
|
requestObject.params = testParams;
|
|
147
148
|
|
|
148
149
|
// call Constraint
|
|
149
|
-
if(typeof
|
|
150
|
+
if(typeof route.constraint === "function"){
|
|
150
151
|
|
|
151
152
|
var newObj = {};
|
|
152
153
|
//tools.combineObjects(newObj, this._master.controllerList);
|
|
@@ -163,7 +164,7 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
163
164
|
|
|
164
165
|
// Wrap constraint execution with error handling
|
|
165
166
|
try {
|
|
166
|
-
|
|
167
|
+
route.constraint.call(newObj, requestObject);
|
|
167
168
|
} catch(constraintError) {
|
|
168
169
|
const routeError = handleRoutingError(
|
|
169
170
|
requestObject.pathName,
|
|
@@ -238,10 +239,10 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
238
239
|
};
|
|
239
240
|
|
|
240
241
|
var loadScopedListClasses = function(){
|
|
241
|
-
for (
|
|
242
|
-
|
|
242
|
+
// FIXED: Use Object.entries() for safe iteration (prevents prototype pollution)
|
|
243
|
+
for (const [key, className] of Object.entries(this._master._scopedList)) {
|
|
243
244
|
this._master.requestList[key] = new className();
|
|
244
|
-
}
|
|
245
|
+
}
|
|
245
246
|
};
|
|
246
247
|
|
|
247
248
|
|
|
@@ -397,25 +398,19 @@ class MasterRouter {
|
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
findMimeType(fileExt){
|
|
400
|
-
if(fileExt){
|
|
401
|
-
var type = undefined;
|
|
402
|
-
var mime = this.mimeTypes;
|
|
403
|
-
for(var i in mime) {
|
|
404
|
-
|
|
405
|
-
if("." + i === fileExt){
|
|
406
|
-
type = mime[i];
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if(type === undefined){
|
|
410
|
-
return false;
|
|
411
|
-
}
|
|
412
|
-
else{
|
|
413
|
-
return type;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
else{
|
|
401
|
+
if(!fileExt){
|
|
417
402
|
return false;
|
|
418
403
|
}
|
|
404
|
+
|
|
405
|
+
// FIXED: O(1) direct lookup instead of O(n) loop
|
|
406
|
+
// Remove leading dot if present for consistent lookup
|
|
407
|
+
const ext = fileExt.startsWith('.') ? fileExt.slice(1) : fileExt;
|
|
408
|
+
|
|
409
|
+
// Direct object access - constant time complexity
|
|
410
|
+
const type = this.mimeTypes[ext];
|
|
411
|
+
|
|
412
|
+
// Return the MIME type or false if not found
|
|
413
|
+
return type || false;
|
|
419
414
|
}
|
|
420
415
|
|
|
421
416
|
_call(requestObject){
|