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.
@@ -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, '&amp;')
310
+ .replace(/</g, '&lt;')
311
+ .replace(/>/g, '&gt;')
312
+ .replace(/"/g, '&quot;')
313
+ .replace(/'/g, '&#x27;')
314
+ .replace(/\//g, '&#x2F;');
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(/&amp;/g, '&')
327
+ .replace(/&lt;/g, '<')
328
+ .replace(/&gt;/g, '>')
329
+ .replace(/&quot;/g, '"')
330
+ .replace(/&#x27;/g, "'")
331
+ .replace(/&#x2F;/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.3
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
- parsed = known ? _[str] : (_[str] = this.parse(str)),
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;