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.
- package/.claude/settings.local.json +6 -1
- package/.eslintrc.json +50 -0
- package/.github/workflows/ci.yml +317 -0
- package/.prettierrc +10 -0
- package/CHANGES.md +296 -0
- package/DEPLOYMENT.md +956 -0
- package/FIXES_APPLIED.md +378 -0
- package/FORTUNE_500_UPGRADE.md +863 -0
- package/MasterAction.js +10 -263
- package/MasterControl.js +226 -43
- package/MasterRequest.js +42 -1
- package/MasterRouter.js +42 -37
- package/PERFORMANCE_SECURITY_AUDIT.md +677 -0
- package/README.md +602 -71
- package/SENIOR_ENGINEER_AUDIT.md +2477 -0
- package/VERIFICATION_CHECKLIST.md +726 -0
- package/error/README.md +2452 -0
- package/monitoring/HealthCheck.js +347 -0
- package/monitoring/PrometheusExporter.js +416 -0
- package/monitoring/README.md +3112 -0
- package/package.json +64 -11
- package/security/MasterValidator.js +140 -10
- package/security/README.md +1805 -0
- package/security/adapters/RedisCSRFStore.js +428 -0
- package/security/adapters/RedisRateLimiter.js +462 -0
- package/security/adapters/RedisSessionStore.js +476 -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/error/ErrorBoundary.js +0 -353
- package/error/HydrationMismatch.js +0 -265
- package/error/MasterError.js +0 -240
- package/error/MasterError.js.tmp +0 -0
- package/error/MasterErrorRenderer.js +0 -536
- package/error/MasterErrorRenderer.js.tmp +0 -0
- package/error/SSRErrorHandler.js +0 -273
- package/ssr/hydration-client.js +0 -93
- package/ssr/runtime-ssr.cjs +0 -553
- 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
|
-
|
|
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
|
-
//
|
|
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,
|
|
@@ -237,11 +238,15 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
237
238
|
}
|
|
238
239
|
};
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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();
|