mastercontroller 1.2.11 → 1.2.13
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/CSPConfig.js +319 -0
- package/EventHandlerValidator.js +464 -0
- package/MasterAction.js +296 -72
- package/MasterBackendErrorHandler.js +769 -0
- package/MasterBenchmark.js +89 -0
- package/MasterBuildOptimizer.js +376 -0
- package/MasterBundleAnalyzer.js +108 -0
- package/MasterCache.js +400 -0
- package/MasterControl.js +77 -7
- package/MasterErrorHandler.js +487 -0
- package/MasterErrorLogger.js +360 -0
- package/MasterErrorMiddleware.js +407 -0
- package/MasterHtml.js +101 -14
- package/MasterMemoryMonitor.js +188 -0
- package/MasterProfiler.js +409 -0
- package/MasterRouter.js +273 -66
- package/MasterSanitizer.js +429 -0
- package/MasterTemplate.js +96 -3
- package/MasterValidator.js +546 -0
- package/README.md +0 -44
- package/SecurityMiddleware.js +486 -0
- package/SessionSecurity.js +416 -0
- package/package.json +2 -2
- package/ssr/ErrorBoundary.js +353 -0
- package/ssr/HTMLUtils.js +15 -0
- package/ssr/HydrationMismatch.js +265 -0
- package/ssr/PerformanceMonitor.js +233 -0
- package/ssr/SSRErrorHandler.js +273 -0
- package/ssr/hydration-client.js +93 -0
- package/ssr/runtime-ssr.cjs +553 -0
- package/ssr/ssr-shims.js +73 -0
- package/examples/FileServingExample.js +0 -88
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// version 1.0.0
|
|
2
|
+
// MasterController HTML Sanitizer - XSS Protection
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Comprehensive HTML sanitization to prevent XSS attacks
|
|
6
|
+
* Protects against: script injection, event handler injection, data URI attacks,
|
|
7
|
+
* CSS injection, iframe attacks, form hijacking, meta tag injection
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { logger } = require('./MasterErrorLogger');
|
|
11
|
+
|
|
12
|
+
// Dangerous HTML tags that should be removed
|
|
13
|
+
const DANGEROUS_TAGS = [
|
|
14
|
+
'script', 'iframe', 'object', 'embed', 'applet',
|
|
15
|
+
'link', 'style', 'meta', 'base', 'form',
|
|
16
|
+
'input', 'button', 'textarea', 'select', 'option',
|
|
17
|
+
'frame', 'frameset', 'layer', 'ilayer',
|
|
18
|
+
'bgsound', 'xml', 'plaintext', 'xmp'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Dangerous attributes that can execute code
|
|
22
|
+
const DANGEROUS_ATTRIBUTES = [
|
|
23
|
+
'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout',
|
|
24
|
+
'onmousemove', 'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup',
|
|
25
|
+
'onkeypress', 'onfocus', 'onblur', 'onchange', 'onsubmit',
|
|
26
|
+
'onreset', 'onselect', 'onabort', 'ondrag', 'ondrop',
|
|
27
|
+
'ondragstart', 'ondragend', 'ondragover', 'ondragleave',
|
|
28
|
+
'ondragenter', 'onwheel', 'onscroll', 'ontouchstart',
|
|
29
|
+
'ontouchend', 'ontouchmove', 'onanimationstart', 'onanimationend',
|
|
30
|
+
'ontransitionend', 'formaction', 'action', 'poster'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Dangerous URL protocols
|
|
34
|
+
const DANGEROUS_PROTOCOLS = [
|
|
35
|
+
'javascript:', 'data:', 'vbscript:', 'file:', 'about:',
|
|
36
|
+
'ms-its:', 'mhtml:', 'jar:', 'wyciwyg:'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Allowed tags for user-generated content (whitelist approach)
|
|
40
|
+
const ALLOWED_TAGS = [
|
|
41
|
+
'p', 'br', 'strong', 'em', 'u', 'b', 'i', 'span', 'div',
|
|
42
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
43
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
44
|
+
'blockquote', 'pre', 'code', 'hr',
|
|
45
|
+
'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
|
46
|
+
'a', 'img', 'video', 'audio', 'source'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Allowed attributes for each tag
|
|
50
|
+
const ALLOWED_ATTRIBUTES = {
|
|
51
|
+
'a': ['href', 'title', 'rel', 'target'],
|
|
52
|
+
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
53
|
+
'video': ['src', 'controls', 'width', 'height', 'poster'],
|
|
54
|
+
'audio': ['src', 'controls'],
|
|
55
|
+
'source': ['src', 'type'],
|
|
56
|
+
'div': ['class', 'id'],
|
|
57
|
+
'span': ['class', 'id'],
|
|
58
|
+
'p': ['class', 'id'],
|
|
59
|
+
'table': ['class', 'id'],
|
|
60
|
+
'td': ['colspan', 'rowspan'],
|
|
61
|
+
'th': ['colspan', 'rowspan'],
|
|
62
|
+
'h1': ['class', 'id'],
|
|
63
|
+
'h2': ['class', 'id'],
|
|
64
|
+
'h3': ['class', 'id'],
|
|
65
|
+
'h4': ['class', 'id'],
|
|
66
|
+
'h5': ['class', 'id'],
|
|
67
|
+
'h6': ['class', 'id']
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class MasterSanitizer {
|
|
71
|
+
constructor(options = {}) {
|
|
72
|
+
this.allowedTags = options.allowedTags || ALLOWED_TAGS;
|
|
73
|
+
this.allowedAttributes = options.allowedAttributes || ALLOWED_ATTRIBUTES;
|
|
74
|
+
this.stripDisallowed = options.stripDisallowed !== false;
|
|
75
|
+
this.logViolations = options.logViolations !== false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sanitize HTML string to prevent XSS attacks
|
|
80
|
+
* @param {string} html - HTML string to sanitize
|
|
81
|
+
* @param {object} options - Sanitization options
|
|
82
|
+
* @returns {string} - Sanitized HTML
|
|
83
|
+
*/
|
|
84
|
+
sanitizeHTML(html, options = {}) {
|
|
85
|
+
if (!html || typeof html !== 'string') {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
let sanitized = html;
|
|
91
|
+
|
|
92
|
+
// Remove dangerous tags
|
|
93
|
+
sanitized = this._removeDangerousTags(sanitized);
|
|
94
|
+
|
|
95
|
+
// Remove dangerous attributes
|
|
96
|
+
sanitized = this._removeDangerousAttributes(sanitized);
|
|
97
|
+
|
|
98
|
+
// Sanitize URLs in href/src attributes
|
|
99
|
+
sanitized = this._sanitizeURLs(sanitized);
|
|
100
|
+
|
|
101
|
+
// Remove comments (can hide XSS)
|
|
102
|
+
sanitized = this._removeComments(sanitized);
|
|
103
|
+
|
|
104
|
+
// Apply whitelist if strict mode
|
|
105
|
+
if (options.strict) {
|
|
106
|
+
sanitized = this._applyWhitelist(sanitized);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Encode special characters
|
|
110
|
+
if (options.encode) {
|
|
111
|
+
sanitized = this.encodeHTML(sanitized);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return sanitized;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.error({
|
|
117
|
+
code: 'MC_ERR_SANITIZATION',
|
|
118
|
+
message: 'HTML sanitization failed',
|
|
119
|
+
error: error.message
|
|
120
|
+
});
|
|
121
|
+
// Return empty string on error to be safe
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remove dangerous HTML tags
|
|
128
|
+
*/
|
|
129
|
+
_removeDangerousTags(html) {
|
|
130
|
+
let sanitized = html;
|
|
131
|
+
|
|
132
|
+
DANGEROUS_TAGS.forEach(tag => {
|
|
133
|
+
// Remove opening and closing tags
|
|
134
|
+
const regex = new RegExp(`<${tag}[^>]*>.*?<\/${tag}>`, 'gis');
|
|
135
|
+
sanitized = sanitized.replace(regex, '');
|
|
136
|
+
|
|
137
|
+
// Remove self-closing tags
|
|
138
|
+
const selfClosing = new RegExp(`<${tag}[^>]*\/>`, 'gi');
|
|
139
|
+
sanitized = sanitized.replace(selfClosing, '');
|
|
140
|
+
|
|
141
|
+
// Remove unclosed tags
|
|
142
|
+
const unclosed = new RegExp(`<${tag}[^>]*>`, 'gi');
|
|
143
|
+
sanitized = sanitized.replace(unclosed, '');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Log violation
|
|
147
|
+
if (sanitized !== html && this.logViolations) {
|
|
148
|
+
logger.warn({
|
|
149
|
+
code: 'MC_WARN_XSS_ATTEMPT',
|
|
150
|
+
message: 'Dangerous HTML tags removed',
|
|
151
|
+
tags: DANGEROUS_TAGS.filter(tag => html.toLowerCase().includes(`<${tag}`))
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return sanitized;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove dangerous attributes that can execute code
|
|
160
|
+
*/
|
|
161
|
+
_removeDangerousAttributes(html) {
|
|
162
|
+
let sanitized = html;
|
|
163
|
+
|
|
164
|
+
DANGEROUS_ATTRIBUTES.forEach(attr => {
|
|
165
|
+
// Remove attribute with any value
|
|
166
|
+
const regex = new RegExp(`\\s${attr}\\s*=\\s*["'][^"']*["']`, 'gi');
|
|
167
|
+
sanitized = sanitized.replace(regex, '');
|
|
168
|
+
|
|
169
|
+
// Remove attribute without quotes
|
|
170
|
+
const noQuotes = new RegExp(`\\s${attr}\\s*=\\s*[^\\s>]+`, 'gi');
|
|
171
|
+
sanitized = sanitized.replace(noQuotes, '');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Log violation
|
|
175
|
+
if (sanitized !== html && this.logViolations) {
|
|
176
|
+
logger.warn({
|
|
177
|
+
code: 'MC_WARN_XSS_ATTEMPT',
|
|
178
|
+
message: 'Dangerous HTML attributes removed',
|
|
179
|
+
attributes: DANGEROUS_ATTRIBUTES.filter(attr =>
|
|
180
|
+
new RegExp(`\\s${attr}\\s*=`, 'i').test(html)
|
|
181
|
+
)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return sanitized;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Sanitize URLs to prevent javascript: and data: protocol attacks
|
|
190
|
+
*/
|
|
191
|
+
_sanitizeURLs(html) {
|
|
192
|
+
let sanitized = html;
|
|
193
|
+
|
|
194
|
+
// Sanitize href attributes
|
|
195
|
+
sanitized = sanitized.replace(/href\s*=\s*["']([^"']*)["']/gi, (match, url) => {
|
|
196
|
+
const cleanUrl = this._cleanURL(url);
|
|
197
|
+
return `href="${cleanUrl}"`;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Sanitize src attributes
|
|
201
|
+
sanitized = sanitized.replace(/src\s*=\s*["']([^"']*)["']/gi, (match, url) => {
|
|
202
|
+
const cleanUrl = this._cleanURL(url);
|
|
203
|
+
return `src="${cleanUrl}"`;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return sanitized;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clean individual URL
|
|
211
|
+
*/
|
|
212
|
+
_cleanURL(url) {
|
|
213
|
+
if (!url) return '';
|
|
214
|
+
|
|
215
|
+
const trimmed = url.trim().toLowerCase();
|
|
216
|
+
|
|
217
|
+
// Check for dangerous protocols
|
|
218
|
+
for (const protocol of DANGEROUS_PROTOCOLS) {
|
|
219
|
+
if (trimmed.startsWith(protocol)) {
|
|
220
|
+
if (this.logViolations) {
|
|
221
|
+
logger.warn({
|
|
222
|
+
code: 'MC_WARN_XSS_ATTEMPT',
|
|
223
|
+
message: 'Dangerous URL protocol blocked',
|
|
224
|
+
url: url,
|
|
225
|
+
protocol: protocol
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return '#';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check for encoded javascript
|
|
233
|
+
if (trimmed.includes('%6a%61%76%61%73%63%72%69%70%74') || // javascript
|
|
234
|
+
trimmed.includes('&#')) { // HTML entities
|
|
235
|
+
if (this.logViolations) {
|
|
236
|
+
logger.warn({
|
|
237
|
+
code: 'MC_WARN_XSS_ATTEMPT',
|
|
238
|
+
message: 'Encoded malicious URL blocked',
|
|
239
|
+
url: url
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return '#';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return url;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Remove HTML comments (can hide XSS)
|
|
250
|
+
*/
|
|
251
|
+
_removeComments(html) {
|
|
252
|
+
return html.replace(/<!--[\s\S]*?-->/g, '');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Apply whitelist - only allow specific tags and attributes
|
|
257
|
+
*/
|
|
258
|
+
_applyWhitelist(html) {
|
|
259
|
+
// This is a simple implementation - for production use a library like DOMPurify
|
|
260
|
+
let sanitized = html;
|
|
261
|
+
|
|
262
|
+
// Remove all tags not in whitelist
|
|
263
|
+
const tagRegex = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
|
|
264
|
+
sanitized = sanitized.replace(tagRegex, (match, tagName) => {
|
|
265
|
+
const tag = tagName.toLowerCase();
|
|
266
|
+
|
|
267
|
+
// Check if tag is allowed
|
|
268
|
+
if (!this.allowedTags.includes(tag)) {
|
|
269
|
+
if (this.logViolations) {
|
|
270
|
+
logger.warn({
|
|
271
|
+
code: 'MC_WARN_TAG_BLOCKED',
|
|
272
|
+
message: `Tag not in whitelist: ${tag}`
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Filter attributes
|
|
279
|
+
const allowedAttrs = this.allowedAttributes[tag] || [];
|
|
280
|
+
if (allowedAttrs.length === 0) {
|
|
281
|
+
// Tag has no allowed attributes, return clean tag
|
|
282
|
+
return match.startsWith('</') ? `</${tag}>` : `<${tag}>`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Keep only allowed attributes
|
|
286
|
+
let cleanTag = match.replace(/\s+([a-z-]+)\s*=\s*["']([^"']*)["']/gi, (attrMatch, attrName, attrValue) => {
|
|
287
|
+
if (allowedAttrs.includes(attrName.toLowerCase())) {
|
|
288
|
+
return ` ${attrName}="${attrValue}"`;
|
|
289
|
+
}
|
|
290
|
+
return '';
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return cleanTag;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return sanitized;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Encode HTML special characters
|
|
301
|
+
* Use this for displaying user input as text (not HTML)
|
|
302
|
+
*/
|
|
303
|
+
encodeHTML(str) {
|
|
304
|
+
if (!str || typeof str !== 'string') {
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return str
|
|
309
|
+
.replace(/&/g, '&')
|
|
310
|
+
.replace(/</g, '<')
|
|
311
|
+
.replace(/>/g, '>')
|
|
312
|
+
.replace(/"/g, '"')
|
|
313
|
+
.replace(/'/g, ''')
|
|
314
|
+
.replace(/\//g, '/');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Decode HTML entities
|
|
319
|
+
*/
|
|
320
|
+
decodeHTML(str) {
|
|
321
|
+
if (!str || typeof str !== 'string') {
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return str
|
|
326
|
+
.replace(/&/g, '&')
|
|
327
|
+
.replace(/</g, '<')
|
|
328
|
+
.replace(/>/g, '>')
|
|
329
|
+
.replace(/"/g, '"')
|
|
330
|
+
.replace(/'/g, "'")
|
|
331
|
+
.replace(///g, '/');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Safe innerHTML replacement
|
|
336
|
+
* Use this instead of element.innerHTML for user content
|
|
337
|
+
*/
|
|
338
|
+
safeSetInnerHTML(element, html, options = {}) {
|
|
339
|
+
if (!element) return;
|
|
340
|
+
|
|
341
|
+
const sanitized = this.sanitizeHTML(html, options);
|
|
342
|
+
element.innerHTML = sanitized;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Sanitize component props/attributes
|
|
347
|
+
*/
|
|
348
|
+
sanitizeProps(props) {
|
|
349
|
+
if (!props || typeof props !== 'object') {
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const sanitized = {};
|
|
354
|
+
|
|
355
|
+
for (const [key, value] of Object.entries(props)) {
|
|
356
|
+
// Skip internal props
|
|
357
|
+
if (key.startsWith('_') || key.startsWith('__')) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Sanitize string values
|
|
362
|
+
if (typeof value === 'string') {
|
|
363
|
+
sanitized[key] = this.encodeHTML(value);
|
|
364
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
365
|
+
sanitized[key] = this.sanitizeProps(value);
|
|
366
|
+
} else {
|
|
367
|
+
sanitized[key] = value;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return sanitized;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Sanitize text content (for text nodes, not HTML)
|
|
376
|
+
*/
|
|
377
|
+
sanitizeText(text) {
|
|
378
|
+
if (!text || typeof text !== 'string') {
|
|
379
|
+
return '';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return this.encodeHTML(text);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Create singleton instance
|
|
387
|
+
const sanitizer = new MasterSanitizer();
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Quick sanitization functions for common use cases
|
|
391
|
+
*/
|
|
392
|
+
|
|
393
|
+
// Sanitize user-generated HTML (strict mode)
|
|
394
|
+
function sanitizeUserHTML(html) {
|
|
395
|
+
return sanitizer.sanitizeHTML(html, { strict: true });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Sanitize template HTML (less strict, but remove dangerous content)
|
|
399
|
+
function sanitizeTemplateHTML(html) {
|
|
400
|
+
return sanitizer.sanitizeHTML(html, { strict: false });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Encode text to display as-is (not as HTML)
|
|
404
|
+
function escapeHTML(text) {
|
|
405
|
+
return sanitizer.encodeHTML(text);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Safe prop sanitization
|
|
409
|
+
function sanitizeProps(props) {
|
|
410
|
+
return sanitizer.sanitizeProps(props);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Safe innerHTML setter
|
|
414
|
+
function safeInnerHTML(element, html) {
|
|
415
|
+
return sanitizer.safeSetInnerHTML(element, html, { strict: true });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = {
|
|
419
|
+
MasterSanitizer,
|
|
420
|
+
sanitizer,
|
|
421
|
+
sanitizeUserHTML,
|
|
422
|
+
sanitizeTemplateHTML,
|
|
423
|
+
escapeHTML,
|
|
424
|
+
sanitizeProps,
|
|
425
|
+
safeInnerHTML,
|
|
426
|
+
DANGEROUS_TAGS,
|
|
427
|
+
DANGEROUS_ATTRIBUTES,
|
|
428
|
+
DANGEROUS_PROTOCOLS
|
|
429
|
+
};
|
package/MasterTemplate.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
// version 0.0.
|
|
1
|
+
// version 0.0.4
|
|
2
2
|
// https://github.com/WebReflection/backtick-template
|
|
3
3
|
// https://stackoverflow.com/questions/29182244/convert-a-string-to-a-template-string
|
|
4
|
+
|
|
5
|
+
// Security - Template injection prevention
|
|
6
|
+
const { escapeHTML } = require('./MasterSanitizer');
|
|
7
|
+
const { logger } = require('./MasterErrorLogger');
|
|
8
|
+
|
|
4
9
|
var replace = ''.replace;
|
|
5
10
|
|
|
6
11
|
var ca = /[&<>'"]/g;
|
|
@@ -47,8 +52,14 @@ class MasterTemplate{
|
|
|
47
52
|
str = hasTransformer ? $str : fn,
|
|
48
53
|
object = hasTransformer ? $object : $str,
|
|
49
54
|
_ = this._,
|
|
50
|
-
known = _.hasOwnProperty(str)
|
|
51
|
-
|
|
55
|
+
known = _.hasOwnProperty(str);
|
|
56
|
+
|
|
57
|
+
// Security: Validate template for dangerous patterns
|
|
58
|
+
if (!known) {
|
|
59
|
+
this.validateTemplate(str);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var parsed = known ? _[str] : (_[str] = this.parse(str)),
|
|
52
63
|
chunks = parsed.chunks,
|
|
53
64
|
values = parsed.values,
|
|
54
65
|
strings
|
|
@@ -132,6 +143,88 @@ return {chunks: chunks, values: values};
|
|
|
132
143
|
cape(m) {
|
|
133
144
|
return unes[m];
|
|
134
145
|
}
|
|
146
|
+
|
|
147
|
+
// ==================== Security Methods ====================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate template for dangerous patterns
|
|
151
|
+
* Prevents template injection attacks
|
|
152
|
+
*/
|
|
153
|
+
validateTemplate(template) {
|
|
154
|
+
if (!template || typeof template !== 'string') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
|
|
159
|
+
|
|
160
|
+
// Dangerous patterns in templates
|
|
161
|
+
const dangerousPatterns = [
|
|
162
|
+
{ pattern: /\$\{.*__proto__/gi, name: 'Prototype pollution' },
|
|
163
|
+
{ pattern: /\$\{.*constructor.*\(/gi, name: 'Constructor access' },
|
|
164
|
+
{ pattern: /\$\{.*\beval\s*\(/gi, name: 'eval() usage' },
|
|
165
|
+
{ pattern: /\$\{.*Function\s*\(/gi, name: 'Function constructor' },
|
|
166
|
+
{ pattern: /\$\{.*require\s*\(/gi, name: 'require() usage' },
|
|
167
|
+
{ pattern: /\$\{.*import\s*\(/gi, name: 'import() usage' },
|
|
168
|
+
{ pattern: /\$\{.*process\./gi, name: 'Process access' },
|
|
169
|
+
{ pattern: /\$\{.*global\./gi, name: 'Global object access' },
|
|
170
|
+
{ pattern: /\$\{.*\bfs\./gi, name: 'File system access' },
|
|
171
|
+
{ pattern: /\$\{.*child_process/gi, name: 'Child process access' }
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
for (const { pattern, name } of dangerousPatterns) {
|
|
175
|
+
if (pattern.test(template)) {
|
|
176
|
+
logger.error({
|
|
177
|
+
code: 'MC_SECURITY_TEMPLATE_INJECTION',
|
|
178
|
+
message: `Dangerous template pattern detected: ${name}`,
|
|
179
|
+
pattern: pattern.toString(),
|
|
180
|
+
template: template.substring(0, 200) // Log first 200 chars only
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (isDevelopment) {
|
|
184
|
+
throw new Error(`[MasterController Security] Template injection attempt detected: ${name}\nPattern: ${pattern}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// In production, sanitize by removing the dangerous expression
|
|
188
|
+
template = template.replace(pattern, '${/* REMOVED: Security risk */}');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return template;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Sanitize template variables before rendering
|
|
197
|
+
* Call this on user-provided data
|
|
198
|
+
*/
|
|
199
|
+
sanitizeVariable(value) {
|
|
200
|
+
if (value === null || value === undefined) {
|
|
201
|
+
return '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (typeof value === 'string') {
|
|
205
|
+
return escapeHTML(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (typeof value === 'object') {
|
|
209
|
+
// Prevent prototype pollution
|
|
210
|
+
if (value.__proto__ || value.constructor) {
|
|
211
|
+
logger.warn({
|
|
212
|
+
code: 'MC_SECURITY_OBJECT_POLLUTION',
|
|
213
|
+
message: 'Attempted to pass object with prototype/constructor to template'
|
|
214
|
+
});
|
|
215
|
+
return '[Object]';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Safely stringify
|
|
219
|
+
try {
|
|
220
|
+
return JSON.stringify(value);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return '[Object]';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return String(value);
|
|
227
|
+
}
|
|
135
228
|
}
|
|
136
229
|
|
|
137
230
|
module.exports = MasterTemplate;
|