mastercontroller 1.3.1 → 1.3.2

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.
@@ -5,7 +5,9 @@
5
5
  "Bash(mv:*)",
6
6
  "Bash(node -e:*)",
7
7
  "Bash(find:*)",
8
- "Bash(node -c:*)"
8
+ "Bash(node -c:*)",
9
+ "Bash(grep:*)",
10
+ "Bash(ls:*)"
9
11
  ],
10
12
  "deny": [],
11
13
  "ask": []
package/MasterAction.js CHANGED
@@ -46,22 +46,72 @@ class MasterAction{
46
46
 
47
47
 
48
48
  returnJson(data){
49
- var json = JSON.stringify(data);
50
- if (!this.__response._headerSent) {
51
- this.__response.writeHead(200, {'Content-Type': 'application/json'});
52
- this.__response.end(json);
49
+ try {
50
+ const json = JSON.stringify(data);
51
+ // FIXED: Check both _headerSent and headersSent for compatibility
52
+ if (!this.__response._headerSent && !this.__response.headersSent) {
53
+ this.__response.writeHead(200, {'Content-Type': 'application/json'});
54
+ this.__response.end(json);
55
+ } else {
56
+ logger.warn({
57
+ code: 'MC_WARN_HEADERS_SENT',
58
+ message: 'Attempted to send JSON but headers already sent'
59
+ });
60
+ }
61
+ } catch (error) {
62
+ logger.error({
63
+ code: 'MC_ERR_JSON_SEND',
64
+ message: 'Failed to send JSON response',
65
+ error: error.message,
66
+ stack: error.stack
67
+ });
53
68
  }
54
69
  }
55
70
 
56
71
  // location starts from the view folder. Ex: partialViews/fileName.html
57
72
  returnPartialView(location, data){
58
- var actionUrl = master.root + location;
59
- var getAction = fileserver.readFileSync(actionUrl, 'utf8');
60
- if(master.overwrite.isTemplate){
61
- return master.overwrite.templateRender( data, "returnPartialView");
73
+ // SECURITY: Validate path to prevent traversal attacks
74
+ if (!location || location.includes('..') || location.includes('~') || path.isAbsolute(location)) {
75
+ logger.warn({
76
+ code: 'MC_SECURITY_PATH_TRAVERSAL',
77
+ message: 'Path traversal attempt blocked in returnPartialView',
78
+ path: location
79
+ });
80
+ this.returnError(400, 'Invalid path');
81
+ return '';
62
82
  }
63
- else{
64
- return temp.htmlBuilder(getAction, data);
83
+
84
+ const actionUrl = path.resolve(master.root, location);
85
+
86
+ // SECURITY: Ensure resolved path is within app root
87
+ if (!actionUrl.startsWith(master.root)) {
88
+ logger.warn({
89
+ code: 'MC_SECURITY_PATH_TRAVERSAL',
90
+ message: 'Path traversal blocked in returnPartialView',
91
+ requestedPath: location,
92
+ resolvedPath: actionUrl
93
+ });
94
+ this.returnError(403, 'Forbidden');
95
+ return '';
96
+ }
97
+
98
+ try {
99
+ const getAction = fileserver.readFileSync(actionUrl, 'utf8');
100
+ if(master.overwrite.isTemplate){
101
+ return master.overwrite.templateRender( data, "returnPartialView");
102
+ }
103
+ else{
104
+ return temp.htmlBuilder(getAction, data);
105
+ }
106
+ } catch (error) {
107
+ logger.error({
108
+ code: 'MC_ERR_PARTIAL_VIEW',
109
+ message: 'Failed to read partial view',
110
+ path: location,
111
+ error: error.message
112
+ });
113
+ this.returnError(404, 'View not found');
114
+ return '';
65
115
  }
66
116
  }
67
117
 
@@ -112,21 +162,34 @@ class MasterAction{
112
162
 
113
163
  // redirects to another action inside the same controller = does not reload the page
114
164
  redirectToAction(namespace, action, type, data, components){
165
+ // FIXED: Declare variables before if/else to avoid undefined reference
166
+ const resp = this.__requestObject.response;
167
+ const req = this.__requestObject.request;
115
168
 
116
- var requestObj = {
169
+ const requestObj = {
117
170
  toController : namespace,
118
171
  toAction : action,
119
172
  type : type,
120
173
  params : data
121
- }
174
+ };
175
+
122
176
  if(components){
123
- var resp = this.__requestObject.response;
124
- var req = this.__requestObject.request;
125
- master.router.currentRoute = {root : `${master.root}/components/${namespace}`, toController : namespace, toAction : action, response : resp, request: req };
177
+ master.router.currentRoute = {
178
+ root : `${master.root}/components/${namespace}`,
179
+ toController : namespace,
180
+ toAction : action,
181
+ response : resp,
182
+ request: req
183
+ };
126
184
  }else{
127
- master.router.currentRoute = {root : `${master.root}/${namespace}`, toController : namespace, toAction : action, response : resp, request: req };
185
+ master.router.currentRoute = {
186
+ root : `${master.root}/${namespace}`,
187
+ toController : namespace,
188
+ toAction : action,
189
+ response : resp,
190
+ request: req
191
+ };
128
192
  }
129
-
130
193
 
131
194
  master.router._call(requestObj);
132
195
  }
@@ -176,11 +239,45 @@ class MasterAction{
176
239
  }
177
240
 
178
241
  returnViewWithoutEngine(location){
179
- var actionUrl = master.root + location;
180
- var masterView = fileserver.readFileSync(actionUrl, 'utf8');
181
- if (!this.__requestObject.response._headerSent) {
182
- this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
183
- this.__requestObject.response.end(masterView);
242
+ // SECURITY: Validate path to prevent traversal attacks
243
+ if (!location || location.includes('..') || location.includes('~') || path.isAbsolute(location)) {
244
+ logger.warn({
245
+ code: 'MC_SECURITY_PATH_TRAVERSAL',
246
+ message: 'Path traversal attempt blocked in returnViewWithoutEngine',
247
+ path: location
248
+ });
249
+ this.returnError(400, 'Invalid path');
250
+ return;
251
+ }
252
+
253
+ const actionUrl = path.resolve(master.root, location);
254
+
255
+ // SECURITY: Ensure resolved path is within app root
256
+ if (!actionUrl.startsWith(master.root)) {
257
+ logger.warn({
258
+ code: 'MC_SECURITY_PATH_TRAVERSAL',
259
+ message: 'Path traversal blocked in returnViewWithoutEngine',
260
+ requestedPath: location,
261
+ resolvedPath: actionUrl
262
+ });
263
+ this.returnError(403, 'Forbidden');
264
+ return;
265
+ }
266
+
267
+ try {
268
+ const masterView = fileserver.readFileSync(actionUrl, 'utf8');
269
+ if (!this.__requestObject.response._headerSent) {
270
+ this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
271
+ this.__requestObject.response.end(masterView);
272
+ }
273
+ } catch (error) {
274
+ logger.error({
275
+ code: 'MC_ERR_VIEW_READ',
276
+ message: 'Failed to read view file',
277
+ path: location,
278
+ error: error.message
279
+ });
280
+ this.returnError(404, 'View not found');
184
281
  }
185
282
  }
186
283
 
@@ -448,6 +545,7 @@ class MasterAction{
448
545
  /**
449
546
  * Require HTTPS for this action
450
547
  * Usage: if (!this.requireHTTPS()) return;
548
+ * FIXED: Uses configured hostname, not unvalidated Host header
451
549
  */
452
550
  requireHTTPS() {
453
551
  if (!this.isSecure()) {
@@ -457,7 +555,23 @@ class MasterAction{
457
555
  path: this.__requestObject.pathName
458
556
  });
459
557
 
460
- const httpsUrl = `https://${this.__requestObject.request.headers.host}${this.__requestObject.pathName}`;
558
+ // SECURITY FIX: Never use Host header from request (open redirect vulnerability)
559
+ // Use configured hostname instead
560
+ const configuredHost = master.env?.server?.hostname || 'localhost';
561
+ const httpsPort = master.env?.server?.httpsPort || 443;
562
+ const port = httpsPort === 443 ? '' : `:${httpsPort}`;
563
+
564
+ // Validate configured host exists
565
+ if (!configuredHost || configuredHost === 'localhost') {
566
+ logger.error({
567
+ code: 'MC_CONFIG_MISSING_HOSTNAME',
568
+ message: 'requireHTTPS called but no hostname configured in master.env.server.hostname'
569
+ });
570
+ this.returnError(500, 'Server misconfiguration');
571
+ return false;
572
+ }
573
+
574
+ const httpsUrl = `https://${configuredHost}${port}${this.__requestObject.pathName}`;
461
575
  this.redirectTo(httpsUrl);
462
576
  return false;
463
577
  }
@@ -1,98 +1,203 @@
1
- // version 1.7
1
+ // version 2.0 - FIXED: Instance-level filters, async support, multiple filters
2
2
  var master = require('./MasterControl');
3
-
4
- var _beforeActionFunc = {
5
- namespace : "",
6
- actionList : "",
7
- callBack : "",
8
- that : ""
9
- };
10
- var _afterActionFunc = {
11
- namespace : "",
12
- actionList : "",
13
- callBack : "",
14
- that : ""
15
- };
16
- var emit = "";
3
+ const { logger } = require('./error/MasterErrorLogger');
17
4
 
18
5
  class MasterActionFilters {
19
6
 
20
- // add function to list
21
- beforeAction(actionlist, func){
22
- if (typeof func === 'function') {
23
- _beforeActionFunc = {
24
- namespace : this.__namespace,
25
- actionList : actionlist,
26
- callBack : func,
27
- that : this
28
- };
29
- }
30
- else{
31
- master.error.log("beforeAction callback not a function", "warn");
32
- }
33
-
34
- }
35
-
36
- // add function to list
37
- afterAction(actionlist, func){
38
- if (typeof func === 'function') {
39
- _afterActionFunc = {
40
- namespace : this.__namespace,
41
- actionList : actionlist,
42
- callBack : func,
43
- that : this
44
- };
45
- }
46
- else{
47
- master.error.log("afterAction callback not a function", "warn");
48
- }
49
-
50
- }
51
-
52
- // check to see if that controller has a before Action method.
53
- __hasBeforeAction(obj, request){
54
- var flag = false;
55
- if(_beforeActionFunc.namespace === obj.__namespace){
56
- for (var a = 0; a < _beforeActionFunc.actionList.length; a++) {
57
- var action = request.toAction.replace(/\s/g, '');
58
- var incomingAction = _beforeActionFunc.actionList[a].replace(/\s/g, '');
59
- if(incomingAction === action){
60
- flag = true;
61
- }
62
- }
63
- }
64
- return flag;
65
- }
66
-
67
- __callBeforeAction(obj, request, emitter) {
68
- if(_beforeActionFunc.namespace === obj.__namespace){
69
- _beforeActionFunc.actionList.forEach(action => {
70
- var action = action.replace(/\s/g, '');
71
- var reqAction = request.toAction.replace(/\s/g, '');
72
- if(action === reqAction){
73
- emit = emitter;
74
- // call function inside controller
75
- _beforeActionFunc.callBack.call(_beforeActionFunc.that, request);
76
- }
77
- });
78
- };
79
- }
80
-
81
- __callAfterAction(obj, request) {
82
- if(_afterActionFunc.namespace === obj.__namespace){
83
- _afterActionFunc.actionList.forEach(action => {
84
- var action = action.replace(/\s/g, '');
85
- var reqAction = request.toAction.replace(/\s/g, '');
86
- if(action === reqAction){
87
- _afterActionFunc.callBack.call(_afterActionFunc.that, request);
88
- }
89
- });
90
- };
91
- }
92
-
93
- next(){
94
- emit.emit("controller");
95
- }
7
+ constructor() {
8
+ // FIXED: Instance-level storage instead of module-level
9
+ // Each controller gets its own filter arrays
10
+ this._beforeActionFilters = [];
11
+ this._afterActionFilters = [];
12
+ }
13
+
14
+ // Register a before action filter
15
+ // FIXED: Adds to array instead of overwriting
16
+ beforeAction(actionlist, func){
17
+ if (typeof func !== 'function') {
18
+ master.error.log("beforeAction callback not a function", "warn");
19
+ return;
20
+ }
21
+
22
+ // FIXED: Push to array, don't overwrite
23
+ this._beforeActionFilters.push({
24
+ namespace: this.__namespace,
25
+ actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
26
+ callBack: func,
27
+ that: this
28
+ });
29
+ }
30
+
31
+ // Register an after action filter
32
+ // FIXED: Adds to array instead of overwriting
33
+ afterAction(actionlist, func){
34
+ if (typeof func !== 'function') {
35
+ master.error.log("afterAction callback not a function", "warn");
36
+ return;
37
+ }
38
+
39
+ // FIXED: Push to array, don't overwrite
40
+ this._afterActionFilters.push({
41
+ namespace: this.__namespace,
42
+ actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
43
+ callBack: func,
44
+ that: this
45
+ });
46
+ }
47
+
48
+ // Check if controller has before action filters for this action
49
+ __hasBeforeAction(obj, request){
50
+ if (!this._beforeActionFilters || this._beforeActionFilters.length === 0) {
51
+ return false;
52
+ }
53
+
54
+ return this._beforeActionFilters.some(filter => {
55
+ if (filter.namespace !== obj.__namespace) {
56
+ return false;
57
+ }
58
+
59
+ const requestAction = request.toAction.replace(/\s/g, '');
60
+ return filter.actionList.some(action => {
61
+ const filterAction = action.replace(/\s/g, '');
62
+ return filterAction === requestAction;
63
+ });
64
+ });
65
+ }
66
+
67
+ // Execute all matching before action filters
68
+ // FIXED: Async support, error handling, timeout protection, no variable shadowing
69
+ async __callBeforeAction(obj, request, emitter) {
70
+ if (!this._beforeActionFilters || this._beforeActionFilters.length === 0) {
71
+ return;
72
+ }
73
+
74
+ // Find all matching filters
75
+ const requestAction = request.toAction.replace(/\s/g, '');
76
+ const matchingFilters = this._beforeActionFilters.filter(filter => {
77
+ if (filter.namespace !== obj.__namespace) {
78
+ return false;
79
+ }
80
+
81
+ return filter.actionList.some(actionName => {
82
+ const normalizedAction = actionName.replace(/\s/g, '');
83
+ return normalizedAction === requestAction;
84
+ });
85
+ });
86
+
87
+ // Execute all matching filters in order
88
+ for (const filter of matchingFilters) {
89
+ try {
90
+ // FIXED: Add timeout protection (5 seconds default)
91
+ await this._executeWithTimeout(
92
+ filter.callBack,
93
+ filter.that,
94
+ [request],
95
+ emitter,
96
+ 5000
97
+ );
98
+ } catch (error) {
99
+ // FIXED: Proper error handling
100
+ logger.error({
101
+ code: 'MC_FILTER_ERROR',
102
+ message: 'Error in beforeAction filter',
103
+ namespace: filter.namespace,
104
+ action: requestAction,
105
+ error: error.message,
106
+ stack: error.stack
107
+ });
108
+
109
+ // Send error response
110
+ const res = request.response;
111
+ if (res && !res._headerSent && !res.headersSent) {
112
+ res.writeHead(500, { 'Content-Type': 'application/json' });
113
+ res.end(JSON.stringify({
114
+ error: 'Internal Server Error',
115
+ message: master.environmentType === 'development' ? error.message : 'Filter execution failed'
116
+ }));
117
+ }
118
+
119
+ // Don't continue to other filters if one fails
120
+ throw error;
121
+ }
122
+ }
123
+ }
124
+
125
+ // Execute all matching after action filters
126
+ // FIXED: Async support, error handling, no variable shadowing
127
+ async __callAfterAction(obj, request) {
128
+ if (!this._afterActionFilters || this._afterActionFilters.length === 0) {
129
+ return;
130
+ }
131
+
132
+ // Find all matching filters
133
+ const requestAction = request.toAction.replace(/\s/g, '');
134
+ const matchingFilters = this._afterActionFilters.filter(filter => {
135
+ if (filter.namespace !== obj.__namespace) {
136
+ return false;
137
+ }
138
+
139
+ return filter.actionList.some(actionName => {
140
+ const normalizedAction = actionName.replace(/\s/g, '');
141
+ return normalizedAction === requestAction;
142
+ });
143
+ });
144
+
145
+ // Execute all matching filters in order
146
+ for (const filter of matchingFilters) {
147
+ try {
148
+ // FIXED: Add timeout protection (5 seconds default)
149
+ await this._executeWithTimeout(
150
+ filter.callBack,
151
+ filter.that,
152
+ [request],
153
+ null,
154
+ 5000
155
+ );
156
+ } catch (error) {
157
+ // FIXED: Proper error handling
158
+ logger.error({
159
+ code: 'MC_FILTER_ERROR',
160
+ message: 'Error in afterAction filter',
161
+ namespace: filter.namespace,
162
+ action: requestAction,
163
+ error: error.message,
164
+ stack: error.stack
165
+ });
166
+
167
+ // After filters don't stop execution, just log
168
+ }
169
+ }
170
+ }
171
+
172
+ // FIXED: Execute function with timeout protection
173
+ async _executeWithTimeout(func, context, args, emitter, timeout) {
174
+ // Store emitter in context for next() call
175
+ if (emitter) {
176
+ context.__filterEmitter = emitter;
177
+ }
178
+
179
+ return Promise.race([
180
+ // Execute the filter
181
+ Promise.resolve(func.call(context, ...args)),
182
+ // Timeout promise
183
+ new Promise((_, reject) =>
184
+ setTimeout(() => reject(new Error(`Filter timeout after ${timeout}ms`)), timeout)
185
+ )
186
+ ]);
187
+ }
188
+
189
+ // FIXED: Request-scoped next() function
190
+ next(){
191
+ if (this.__filterEmitter) {
192
+ this.__filterEmitter.emit("controller");
193
+ } else {
194
+ logger.warn({
195
+ code: 'MC_FILTER_WARN',
196
+ message: 'next() called but no emitter available',
197
+ namespace: this.__namespace
198
+ });
199
+ }
200
+ }
96
201
  }
97
202
 
98
- master.extendController( MasterActionFilters);
203
+ master.extendController(MasterActionFilters);