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/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
- var $that = this;
131
- var propertyNames = Object.getOwnPropertyNames( element.__proto__);
184
+ const propertyNames = Object.getOwnPropertyNames(element.__proto__);
132
185
  this.viewList[name] = {};
133
- for(var i in propertyNames){
134
- if(propertyNames[i] !== "constructor"){
135
- if (propertyNames.hasOwnProperty(i)) {
136
- $that.viewList[name][propertyNames[i]] = element[propertyNames[i]];
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
- var $that = this;
146
- var propertyNames = Object.getOwnPropertyNames( element.__proto__);
147
- for(var i in propertyNames){
148
- if(propertyNames[i] !== "constructor"){
149
- if (propertyNames.hasOwnProperty(i)) {
150
- $that.controllerList[propertyNames[i]] = element[propertyNames[i]];
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
- 'MasterHtml': './MasterHtml',
317
- 'MasterTemplate': './MasterTemplate',
318
- 'MasterTools': './MasterTools',
319
- 'TemplateOverwrite': './TemplateOverwrite'
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 view and controller extensions (these extend prototypes, not master instance)
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
- for (var key in $that._scopedList) {
739
- var className = $that._scopedList[key];
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
- // loop through routes
125
- for(var item in routeList){
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: routeList[item].path,
129
- toController: routeList[item].toController,
130
- toAction: routeList[item].toAction,
131
- type: routeList[item].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 = routeList[item].toController;
136
- requestObject.toAction = routeList[item].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, routeList[item].path, testParams);
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 && routeList[item].type === requestObject.type){
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 routeList[item].constraint === "function"){
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
- routeList[item].constraint.call(newObj, requestObject);
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 (var key in this._master._scopedList) {
242
- var className = this._master._scopedList[key];
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){