mastercontroller 1.3.9 → 1.3.12

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.
Files changed (47) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/.eslintrc.json +50 -0
  3. package/.github/workflows/ci.yml +317 -0
  4. package/.prettierrc +10 -0
  5. package/CHANGES.md +296 -0
  6. package/DEPLOYMENT.md +956 -0
  7. package/FIXES_APPLIED.md +378 -0
  8. package/FORTUNE_500_UPGRADE.md +863 -0
  9. package/MasterAction.js +10 -263
  10. package/MasterControl.js +226 -43
  11. package/MasterRequest.js +42 -1
  12. package/MasterRouter.js +42 -37
  13. package/PERFORMANCE_SECURITY_AUDIT.md +677 -0
  14. package/README.md +602 -71
  15. package/SENIOR_ENGINEER_AUDIT.md +2477 -0
  16. package/VERIFICATION_CHECKLIST.md +726 -0
  17. package/error/README.md +2452 -0
  18. package/monitoring/HealthCheck.js +347 -0
  19. package/monitoring/PrometheusExporter.js +416 -0
  20. package/monitoring/README.md +3112 -0
  21. package/package.json +64 -11
  22. package/security/MasterValidator.js +140 -10
  23. package/security/README.md +1805 -0
  24. package/security/adapters/RedisCSRFStore.js +428 -0
  25. package/security/adapters/RedisRateLimiter.js +462 -0
  26. package/security/adapters/RedisSessionStore.js +476 -0
  27. package/MasterCors.js.tmp +0 -0
  28. package/MasterHtml.js +0 -649
  29. package/MasterPipeline.js.tmp +0 -0
  30. package/MasterRequest.js.tmp +0 -0
  31. package/MasterRouter.js.tmp +0 -0
  32. package/MasterSocket.js.tmp +0 -0
  33. package/MasterTemp.js.tmp +0 -0
  34. package/MasterTemplate.js +0 -230
  35. package/MasterTimeout.js.tmp +0 -0
  36. package/TemplateOverwrite.js +0 -41
  37. package/TemplateOverwrite.js.tmp +0 -0
  38. package/error/ErrorBoundary.js +0 -353
  39. package/error/HydrationMismatch.js +0 -265
  40. package/error/MasterError.js +0 -240
  41. package/error/MasterError.js.tmp +0 -0
  42. package/error/MasterErrorRenderer.js +0 -536
  43. package/error/MasterErrorRenderer.js.tmp +0 -0
  44. package/error/SSRErrorHandler.js +0 -273
  45. package/ssr/hydration-client.js +0 -93
  46. package/ssr/runtime-ssr.cjs +0 -553
  47. package/ssr/ssr-shims.js +0 -73
package/MasterRequest.js CHANGED
@@ -26,7 +26,20 @@ class MasterRequest{
26
26
  if(options){
27
27
  this.options = {};
28
28
  this.options.disableFormidableMultipartFormData = options.disableFormidableMultipartFormData === null? false : options.disableFormidableMultipartFormData;
29
- this.options.formidable = options.formidable === null? {}: options.formidable;
29
+
30
+ // CRITICAL FIX: Add file upload limits to prevent DoS attacks
31
+ // Default formidable configuration with security limits
32
+ this.options.formidable = {
33
+ maxFiles: 10, // CRITICAL: Limit number of files per request
34
+ maxFileSize: 50 * 1024 * 1024, // CRITICAL: 50MB per file (was unlimited)
35
+ maxTotalFileSize: 100 * 1024 * 1024, // CRITICAL: 100MB total for all files
36
+ maxFields: 1000, // Limit number of fields
37
+ maxFieldsSize: 20 * 1024 * 1024, // 20MB for all fields combined
38
+ allowEmptyFiles: false, // Reject empty file uploads
39
+ minFileSize: 1, // Minimum 1 byte
40
+ ...(options.formidable || {}) // Allow user overrides
41
+ };
42
+
30
43
  // Body size limits (DoS protection)
31
44
  this.options.maxBodySize = options.maxBodySize || 10 * 1024 * 1024; // 10MB default
32
45
  this.options.maxJsonSize = options.maxJsonSize || 1 * 1024 * 1024; // 1MB default for JSON
@@ -74,6 +87,7 @@ class MasterRequest{
74
87
  // Track uploaded files for cleanup on error
75
88
  const uploadedFiles = [];
76
89
  let uploadAborted = false;
90
+ let totalUploadedSize = 0; // CRITICAL: Track total upload size
77
91
 
78
92
  $that.form.on('field', function(field, value) {
79
93
  $that.parsedURL.formData.fields[field] = value;
@@ -87,6 +101,30 @@ class MasterRequest{
87
101
  $that.form.on('file', function(field, file) {
88
102
  file.extension = file.name === undefined ? path.extname(file.originalFilename) : path.extname(file.name);
89
103
 
104
+ // CRITICAL: Track total uploaded size across all files
105
+ totalUploadedSize += file.size || 0;
106
+
107
+ // CRITICAL: Enforce maxTotalFileSize limit
108
+ const maxTotalSize = $that.options.formidable.maxTotalFileSize || 100 * 1024 * 1024;
109
+ if (totalUploadedSize > maxTotalSize) {
110
+ uploadAborted = true;
111
+ console.error(`[MasterRequest] Total upload size (${totalUploadedSize} bytes) exceeds limit (${maxTotalSize} bytes)`);
112
+
113
+ // Cleanup all uploaded files
114
+ uploadedFiles.forEach(f => {
115
+ if (f.filepath) {
116
+ try {
117
+ $that.deleteFileBuffer(f.filepath);
118
+ } catch (err) {
119
+ console.error('[MasterRequest] Cleanup failed:', err.message);
120
+ }
121
+ }
122
+ });
123
+
124
+ reject(new Error(`Total upload size exceeds limit (${maxTotalSize} bytes)`));
125
+ return;
126
+ }
127
+
90
128
  if(Array.isArray($that.parsedURL.formData.files[field])){
91
129
  $that.parsedURL.formData.files[field].push(file);
92
130
  }
@@ -94,6 +132,9 @@ class MasterRequest{
94
132
  $that.parsedURL.formData.files[field] = [];
95
133
  $that.parsedURL.formData.files[field].push(file);
96
134
  }
135
+
136
+ // CRITICAL: Log file upload for security audit trail
137
+ console.log(`[MasterRequest] File uploaded: ${file.originalFilename || file.name} (${file.size} bytes)`);
97
138
  });
98
139
 
99
140
  $that.form.on('error', function(err) {
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,
@@ -237,11 +238,15 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
237
238
  }
238
239
  };
239
240
 
240
- var loadScopedListClasses = function(){
241
- for (var key in this._master._scopedList) {
242
- var className = this._master._scopedList[key];
243
- this._master.requestList[key] = new className();
244
- };
241
+ // CRITICAL FIX: Race condition - Store scoped services in context instead of shared requestList
242
+ // Previously, concurrent requests would overwrite each other's services in the shared requestList object
243
+ // This caused unpredictable behavior and data corruption in production environments
244
+ var loadScopedListClasses = function(context){
245
+ // FIXED: Use Object.entries() for safe iteration (prevents prototype pollution)
246
+ for (const [key, className] of Object.entries(this._master._scopedList)) {
247
+ // Store scoped services in the context object (request-specific) instead of shared requestList
248
+ context[key] = new className();
249
+ }
245
250
  };
246
251
 
247
252
 
@@ -397,25 +402,19 @@ class MasterRouter {
397
402
  }
398
403
 
399
404
  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{
405
+ if(!fileExt){
417
406
  return false;
418
407
  }
408
+
409
+ // FIXED: O(1) direct lookup instead of O(n) loop
410
+ // Remove leading dot if present for consistent lookup
411
+ const ext = fileExt.startsWith('.') ? fileExt.slice(1) : fileExt;
412
+
413
+ // Direct object access - constant time complexity
414
+ const type = this.mimeTypes[ext];
415
+
416
+ // Return the MIME type or false if not found
417
+ return type || false;
419
418
  }
420
419
 
421
420
  _call(requestObject){
@@ -424,8 +423,11 @@ class MasterRouter {
424
423
  const requestId = `${Date.now()}-${Math.random()}`;
425
424
  performanceTracker.start(requestId, requestObject);
426
425
 
427
- tools.combineObjects(this._master.requestList, requestObject);
428
- requestObject = this._master.requestList;
426
+ // CRITICAL FIX: Create a request-specific context instead of using shared requestList
427
+ // This prevents race conditions where concurrent requests overwrite each other's services
428
+ const requestContext = Object.create(this._master.requestList);
429
+ tools.combineObjects(requestContext, requestObject);
430
+ requestObject = requestContext;
429
431
  var Control = null;
430
432
 
431
433
  try{
@@ -533,9 +535,12 @@ class MasterRouter {
533
535
 
534
536
  load(rr){ // load the the router
535
537
 
536
- loadScopedListClasses.call(this);
537
538
  var $that = this;
538
539
  var requestObject = Object.create(rr);
540
+
541
+ // CRITICAL FIX: Load scoped services into request-specific context
542
+ // Pass requestObject so scoped services are stored per-request, not globally
543
+ loadScopedListClasses.call(this, requestObject);
539
544
  requestObject.pathName = requestObject.pathName.replace(/^\/|\/$/g, '').toLowerCase();
540
545
 
541
546
  var _loadEmit = new EventEmitter();