@designofadecade/server 4.0.0
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/CHANGELOG.md +62 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/client/ApiClient.d.ts +121 -0
- package/dist/client/ApiClient.d.ts.map +1 -0
- package/dist/client/ApiClient.js +289 -0
- package/dist/client/ApiClient.js.map +1 -0
- package/dist/context/Context.d.ts +71 -0
- package/dist/context/Context.d.ts.map +1 -0
- package/dist/context/Context.js +81 -0
- package/dist/context/Context.js.map +1 -0
- package/dist/docs/OpenApiGenerator.d.ts +135 -0
- package/dist/docs/OpenApiGenerator.d.ts.map +1 -0
- package/dist/docs/OpenApiGenerator.js +165 -0
- package/dist/docs/OpenApiGenerator.js.map +1 -0
- package/dist/events/Events.d.ts +52 -0
- package/dist/events/Events.d.ts.map +1 -0
- package/dist/events/Events.js +70 -0
- package/dist/events/Events.js.map +1 -0
- package/dist/events/EventsManager.d.ts +46 -0
- package/dist/events/EventsManager.d.ts.map +1 -0
- package/dist/events/EventsManager.js +137 -0
- package/dist/events/EventsManager.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/local/Local.d.ts +83 -0
- package/dist/local/Local.d.ts.map +1 -0
- package/dist/local/Local.js +114 -0
- package/dist/local/Local.js.map +1 -0
- package/dist/logger/Logger.d.ts +365 -0
- package/dist/logger/Logger.d.ts.map +1 -0
- package/dist/logger/Logger.js +582 -0
- package/dist/logger/Logger.js.map +1 -0
- package/dist/middleware/RequestLogger.d.ts +62 -0
- package/dist/middleware/RequestLogger.d.ts.map +1 -0
- package/dist/middleware/RequestLogger.js +71 -0
- package/dist/middleware/RequestLogger.js.map +1 -0
- package/dist/notifications/Slack.d.ts +19 -0
- package/dist/notifications/Slack.d.ts.map +1 -0
- package/dist/notifications/Slack.js +55 -0
- package/dist/notifications/Slack.js.map +1 -0
- package/dist/router/RouteError.d.ts +21 -0
- package/dist/router/RouteError.d.ts.map +1 -0
- package/dist/router/RouteError.js +31 -0
- package/dist/router/RouteError.js.map +1 -0
- package/dist/router/Router.d.ts +66 -0
- package/dist/router/Router.d.ts.map +1 -0
- package/dist/router/Router.js +327 -0
- package/dist/router/Router.js.map +1 -0
- package/dist/router/Routes.d.ts +30 -0
- package/dist/router/Routes.d.ts.map +1 -0
- package/dist/router/Routes.js +52 -0
- package/dist/router/Routes.js.map +1 -0
- package/dist/router/StaticFileHandler.d.ts +44 -0
- package/dist/router/StaticFileHandler.d.ts.map +1 -0
- package/dist/router/StaticFileHandler.js +148 -0
- package/dist/router/StaticFileHandler.js.map +1 -0
- package/dist/sanitizer/HtmlSanitizer.d.ts +306 -0
- package/dist/sanitizer/HtmlSanitizer.d.ts.map +1 -0
- package/dist/sanitizer/HtmlSanitizer.js +808 -0
- package/dist/sanitizer/HtmlSanitizer.js.map +1 -0
- package/dist/server/Server.d.ts +28 -0
- package/dist/server/Server.d.ts.map +1 -0
- package/dist/server/Server.js +95 -0
- package/dist/server/Server.js.map +1 -0
- package/dist/state/AppState.d.ts +64 -0
- package/dist/state/AppState.d.ts.map +1 -0
- package/dist/state/AppState.js +89 -0
- package/dist/state/AppState.js.map +1 -0
- package/dist/utils/HtmlRenderer.d.ts +6 -0
- package/dist/utils/HtmlRenderer.d.ts.map +1 -0
- package/dist/utils/HtmlRenderer.js +128 -0
- package/dist/utils/HtmlRenderer.js.map +1 -0
- package/dist/websocket/WebSocketMessageFormatter.d.ts +40 -0
- package/dist/websocket/WebSocketMessageFormatter.d.ts.map +1 -0
- package/dist/websocket/WebSocketMessageFormatter.js +99 -0
- package/dist/websocket/WebSocketMessageFormatter.js.map +1 -0
- package/dist/websocket/WebSocketServer.d.ts +14 -0
- package/dist/websocket/WebSocketServer.d.ts.map +1 -0
- package/dist/websocket/WebSocketServer.js +138 -0
- package/dist/websocket/WebSocketServer.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Sanitization Utility
|
|
3
|
+
*
|
|
4
|
+
* Production-grade HTML sanitization to prevent XSS attacks and ensure safe HTML rendering.
|
|
5
|
+
*
|
|
6
|
+
* Security Features:
|
|
7
|
+
* - XSS Prevention: Blocks dangerous protocols (javascript:, data:, vbscript:, etc.)
|
|
8
|
+
* - DoS Protection: Enforces maximum input size limits
|
|
9
|
+
* - Script/Style Removal: Completely removes script and style tags with content
|
|
10
|
+
* - HTML Comment Stripping: Removes comments that could hide malicious content
|
|
11
|
+
* - URL Validation: Validates and sanitizes URLs in anchor tags
|
|
12
|
+
* - Entity Decoding: Safely decodes HTML entities with range validation
|
|
13
|
+
* - Attribute Escaping: Prevents attribute injection attacks
|
|
14
|
+
*
|
|
15
|
+
* @module HtmlSanitizer
|
|
16
|
+
* @version 2.0.0
|
|
17
|
+
*/
|
|
18
|
+
import { logger } from '../logger/Logger.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Configuration Constants
|
|
21
|
+
// ============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Maximum input size to prevent DoS attacks (1MB)
|
|
24
|
+
*/
|
|
25
|
+
const MAX_INPUT_SIZE = 1024 * 1024;
|
|
26
|
+
/**
|
|
27
|
+
* Valid HTML tag name pattern
|
|
28
|
+
*/
|
|
29
|
+
const TAG_NAME_PATTERN = /^[a-z][a-z0-9]*$/i;
|
|
30
|
+
/**
|
|
31
|
+
* Maximum URL length (2048 characters - standard browser limit)
|
|
32
|
+
*/
|
|
33
|
+
const MAX_URL_LENGTH = 2048;
|
|
34
|
+
/**
|
|
35
|
+
* Dangerous protocols that could be used for XSS attacks
|
|
36
|
+
*/
|
|
37
|
+
const DANGEROUS_PROTOCOLS = /^(javascript:|vbscript:|data:|file:|about:|blob:)/i;
|
|
38
|
+
/**
|
|
39
|
+
* Safe protocols allowed in URLs
|
|
40
|
+
*/
|
|
41
|
+
const SAFE_PROTOCOLS = /^(https?:\/\/|mailto:|tel:|sms:|\/|\.\/|\.\.\/|#)/i;
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// HtmlSanitizer Class
|
|
44
|
+
// ============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* HTML Sanitization utility class with static methods for secure HTML processing
|
|
47
|
+
*
|
|
48
|
+
* @class HtmlSanitizer
|
|
49
|
+
*/
|
|
50
|
+
export default class HtmlSanitizer {
|
|
51
|
+
// ========================================================================
|
|
52
|
+
// Public API Methods
|
|
53
|
+
// ========================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Cleans HTML by allowing only specified tags and removing all others.
|
|
56
|
+
*
|
|
57
|
+
* Features:
|
|
58
|
+
* - Strips disallowed tags while preserving text content
|
|
59
|
+
* - Validates and sanitizes URLs in anchor tags
|
|
60
|
+
* - Removes HTML comments, script, and style tags
|
|
61
|
+
* - Enforces DoS protection with size limits
|
|
62
|
+
* - Validates allowed tags is a valid array
|
|
63
|
+
*
|
|
64
|
+
* @param html - The HTML string to clean
|
|
65
|
+
* @param allowedTags - Array of allowed tag names (case-insensitive)
|
|
66
|
+
* @returns Sanitized HTML string with only allowed tags
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Allow only paragraph and bold tags
|
|
70
|
+
* HtmlSanitizer.clean('<p>Hello <b>World</b></p><script>alert("xss")</script>', ['p', 'b']);
|
|
71
|
+
* // Returns: '<p>Hello <b>World</b></p>'
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Sanitize anchor tags with URL validation
|
|
75
|
+
* HtmlSanitizer.clean('<a href="https://example.com">Safe</a><a href="javascript:alert(1)">Unsafe</a>', ['a']);
|
|
76
|
+
* // Returns: '<a href="https://example.com">Safe</a><a>Unsafe</a>'
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // Allow common formatting tags
|
|
80
|
+
* HtmlSanitizer.clean(userInput, ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li']);
|
|
81
|
+
*/
|
|
82
|
+
static clean(html, allowedTags) {
|
|
83
|
+
// ====================================================================
|
|
84
|
+
// Step 1: Input Validation
|
|
85
|
+
// ====================================================================
|
|
86
|
+
if (!html || typeof html !== 'string') {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(allowedTags) || allowedTags.length === 0) {
|
|
90
|
+
logger.warn('No allowed tags provided, stripping all HTML', {
|
|
91
|
+
code: 'SANITIZER_NO_ALLOWED_TAGS',
|
|
92
|
+
source: 'HtmlSanitizer.clean',
|
|
93
|
+
});
|
|
94
|
+
return this.stripAllTags(html);
|
|
95
|
+
}
|
|
96
|
+
// ====================================================================
|
|
97
|
+
// Step 2: Validate and Normalize Allowed Tags
|
|
98
|
+
// ====================================================================
|
|
99
|
+
const validAllowedTags = allowedTags
|
|
100
|
+
.filter((tag) => {
|
|
101
|
+
if (typeof tag !== 'string' || !TAG_NAME_PATTERN.test(tag)) {
|
|
102
|
+
logger.warn('Invalid tag name ignored', {
|
|
103
|
+
code: 'SANITIZER_INVALID_TAG',
|
|
104
|
+
source: 'HtmlSanitizer.clean',
|
|
105
|
+
tag,
|
|
106
|
+
});
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
})
|
|
111
|
+
.map((tag) => tag.toLowerCase());
|
|
112
|
+
if (validAllowedTags.length === 0) {
|
|
113
|
+
logger.warn('No valid allowed tags, stripping all HTML', {
|
|
114
|
+
code: 'SANITIZER_NO_VALID_TAGS',
|
|
115
|
+
source: 'HtmlSanitizer.clean',
|
|
116
|
+
});
|
|
117
|
+
return this.stripAllTags(html);
|
|
118
|
+
}
|
|
119
|
+
// ====================================================================
|
|
120
|
+
// Step 3: DoS Protection
|
|
121
|
+
// ====================================================================
|
|
122
|
+
if (html.length > MAX_INPUT_SIZE) {
|
|
123
|
+
logger.error('Input exceeds maximum size for clean(), truncating', {
|
|
124
|
+
code: 'SANITIZER_INPUT_TOO_LARGE',
|
|
125
|
+
source: 'HtmlSanitizer.clean',
|
|
126
|
+
size: html.length,
|
|
127
|
+
max: MAX_INPUT_SIZE,
|
|
128
|
+
});
|
|
129
|
+
html = html.substring(0, MAX_INPUT_SIZE);
|
|
130
|
+
}
|
|
131
|
+
// ====================================================================
|
|
132
|
+
// Step 4: Remove Dangerous Content
|
|
133
|
+
// ====================================================================
|
|
134
|
+
// Remove HTML comments (could hide malicious content)
|
|
135
|
+
let cleaned = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
136
|
+
// Remove script and style tags entirely (including their content)
|
|
137
|
+
cleaned = cleaned.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, '');
|
|
138
|
+
// ====================================================================
|
|
139
|
+
// Step 5: Process HTML Tags
|
|
140
|
+
// ====================================================================
|
|
141
|
+
cleaned = cleaned.replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, (match, tagName) => {
|
|
142
|
+
const lowerTagName = tagName.toLowerCase();
|
|
143
|
+
// Remove tags not in the allowed list (preserve text content)
|
|
144
|
+
if (!validAllowedTags.includes(lowerTagName)) {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
// Handle closing tags - always allow if opening tag is allowed
|
|
148
|
+
if (match.startsWith('</')) {
|
|
149
|
+
return `</${lowerTagName}>`;
|
|
150
|
+
}
|
|
151
|
+
// Detect self-closing tags
|
|
152
|
+
const isSelfClosing = match.endsWith('/>');
|
|
153
|
+
// Special handling for anchor tags - validate URLs
|
|
154
|
+
if (lowerTagName === 'a') {
|
|
155
|
+
const hrefMatch = match.match(/href\s*=\s*["']([^"']*)["']/i);
|
|
156
|
+
if (hrefMatch && hrefMatch[1]) {
|
|
157
|
+
const url = hrefMatch[1].trim();
|
|
158
|
+
// Validate URL for security
|
|
159
|
+
if (this.isValidUrl(url)) {
|
|
160
|
+
// Escape the URL to prevent attribute injection
|
|
161
|
+
const escapedUrl = this.sanitizeForAttribute(url);
|
|
162
|
+
return `<a href="${escapedUrl}">`;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Invalid URL - remove href but keep anchor tag
|
|
166
|
+
logger.warn('Unsafe URL removed from anchor tag', {
|
|
167
|
+
code: 'SANITIZER_UNSAFE_URL',
|
|
168
|
+
source: 'HtmlSanitizer.clean',
|
|
169
|
+
url,
|
|
170
|
+
});
|
|
171
|
+
return '<a>';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// No href attribute found
|
|
176
|
+
return '<a>';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Handle self-closing tags (br, hr, img)
|
|
180
|
+
if (isSelfClosing && ['br', 'hr', 'img'].includes(lowerTagName)) {
|
|
181
|
+
return `<${lowerTagName} />`;
|
|
182
|
+
}
|
|
183
|
+
// For all other allowed tags, return simple opening tag
|
|
184
|
+
return `<${lowerTagName}>`;
|
|
185
|
+
});
|
|
186
|
+
// ====================================================================
|
|
187
|
+
// Step 6: Final Cleanup
|
|
188
|
+
// ====================================================================
|
|
189
|
+
// Normalize whitespace
|
|
190
|
+
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
191
|
+
return cleaned;
|
|
192
|
+
}
|
|
193
|
+
// ========================================================================
|
|
194
|
+
// Convenience Methods
|
|
195
|
+
// ========================================================================
|
|
196
|
+
/**
|
|
197
|
+
* Sanitizes basic HTML content by allowing only b, i, u, and a tags.
|
|
198
|
+
* Common use case for simple user-generated content.
|
|
199
|
+
*
|
|
200
|
+
* @param html - The HTML string to sanitize
|
|
201
|
+
* @returns Sanitized HTML string with basic formatting
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* HtmlSanitizer.sanitizeBasicHtml('<b>Bold</b> and <i>italic</i> text');
|
|
205
|
+
* // Returns: '<b>Bold</b> and <i>italic</i> text'
|
|
206
|
+
*/
|
|
207
|
+
static sanitizeBasicHtml(html) {
|
|
208
|
+
return this.clean(html, ['b', 'i', 'u', 'a']);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Replaces or adds attributes to specific HTML tags.
|
|
212
|
+
* Useful for adding email-specific attributes like target="_blank" to links.
|
|
213
|
+
*
|
|
214
|
+
* @param html - The HTML string to process
|
|
215
|
+
* @param tagName - The tag name to target (e.g., 'a')
|
|
216
|
+
* @param attributes - Object with attribute key-value pairs to add/replace
|
|
217
|
+
* @returns HTML with updated tag attributes
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* HtmlSanitizer.replaceTagAttributes('<a href="/link">Click</a>', 'a', {
|
|
221
|
+
* target: '_blank',
|
|
222
|
+
* style: 'color: blue;'
|
|
223
|
+
* });
|
|
224
|
+
* // Returns: '<a href="/link" target="_blank" style="color: blue;">Click</a>'
|
|
225
|
+
*/
|
|
226
|
+
static replaceTagAttributes(html, tagName, attributes) {
|
|
227
|
+
if (!html || typeof html !== 'string') {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
if (!tagName || typeof tagName !== 'string') {
|
|
231
|
+
return html;
|
|
232
|
+
}
|
|
233
|
+
if (!attributes || typeof attributes !== 'object') {
|
|
234
|
+
return html;
|
|
235
|
+
}
|
|
236
|
+
const lowerTagName = tagName.toLowerCase();
|
|
237
|
+
// Build attribute string from the attributes object
|
|
238
|
+
const attributeString = Object.entries(attributes)
|
|
239
|
+
.map(([key, value]) => `${key}="${this.sanitizeForAttribute(value)}"`)
|
|
240
|
+
.join(' ');
|
|
241
|
+
// Replace opening tags of the specified type
|
|
242
|
+
return html.replace(new RegExp(`<${lowerTagName}\\b([^>]*)>`, 'gi'), (_match, existingAttrs) => {
|
|
243
|
+
// Parse existing attributes to preserve href, src, etc.
|
|
244
|
+
const preservedAttrs = [];
|
|
245
|
+
// Extract href, src, and other important attributes
|
|
246
|
+
const hrefMatch = existingAttrs.match(/href\s*=\s*["']([^"']*)["']/i);
|
|
247
|
+
const srcMatch = existingAttrs.match(/src\s*=\s*["']([^"']*)["']/i);
|
|
248
|
+
const altMatch = existingAttrs.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
249
|
+
if (hrefMatch)
|
|
250
|
+
preservedAttrs.push(`href="${hrefMatch[1]}"`);
|
|
251
|
+
if (srcMatch)
|
|
252
|
+
preservedAttrs.push(`src="${srcMatch[1]}"`);
|
|
253
|
+
if (altMatch)
|
|
254
|
+
preservedAttrs.push(`alt="${altMatch[1]}"`);
|
|
255
|
+
// Combine preserved and new attributes
|
|
256
|
+
const allAttrs = [...preservedAttrs, attributeString].filter(Boolean).join(' ');
|
|
257
|
+
return allAttrs ? `<${lowerTagName} ${allAttrs}>` : `<${lowerTagName}>`;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Strips all HTML tags from content, leaving only plain text.
|
|
262
|
+
* Removes comments, scripts, styles, and decodes HTML entities.
|
|
263
|
+
*
|
|
264
|
+
* @param html - The HTML string to strip
|
|
265
|
+
* @returns Plain text without any HTML tags
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* HtmlSanitizer.stripAllTags('<p>Hello <b>World</b></p>');
|
|
269
|
+
* // Returns: 'Hello World'
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* HtmlSanitizer.stripAllTags('<script>alert(1)</script>Safe text');
|
|
273
|
+
* // Returns: 'Safe text'
|
|
274
|
+
*/
|
|
275
|
+
static stripAllTags(html) {
|
|
276
|
+
if (!html || typeof html !== 'string') {
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
// DoS protection - reject excessively large inputs
|
|
280
|
+
if (html.length > MAX_INPUT_SIZE) {
|
|
281
|
+
logger.error('Input exceeds maximum size for stripAllTags, truncating', {
|
|
282
|
+
code: 'SANITIZER_STRIP_INPUT_TOO_LARGE',
|
|
283
|
+
source: 'HtmlSanitizer.stripAllTags',
|
|
284
|
+
size: html.length,
|
|
285
|
+
max: MAX_INPUT_SIZE,
|
|
286
|
+
});
|
|
287
|
+
html = html.substring(0, MAX_INPUT_SIZE);
|
|
288
|
+
}
|
|
289
|
+
// Remove HTML comments
|
|
290
|
+
let text = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
291
|
+
// Remove script and style tags with content
|
|
292
|
+
text = text.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, '');
|
|
293
|
+
// Remove all HTML tags
|
|
294
|
+
text = text.replace(/<[^>]*>/g, '');
|
|
295
|
+
// Decode HTML entities
|
|
296
|
+
text = this.decodeHtmlEntities(text);
|
|
297
|
+
// Clean up extra whitespace
|
|
298
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
299
|
+
return text;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Alias for stripAllTags() - strips all HTML tags from content.
|
|
303
|
+
* Provided for convenience and backwards compatibility.
|
|
304
|
+
*
|
|
305
|
+
* @param html - The HTML string to strip
|
|
306
|
+
* @returns Plain text without HTML tags
|
|
307
|
+
* @see stripAllTags
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* HtmlSanitizer.stripAll('<div>Content</div>');
|
|
311
|
+
* // Returns: 'Content'
|
|
312
|
+
*/
|
|
313
|
+
static stripAll(html) {
|
|
314
|
+
return this.stripAllTags(html);
|
|
315
|
+
}
|
|
316
|
+
// ========================================================================
|
|
317
|
+
// Text Escaping Methods
|
|
318
|
+
// ========================================================================
|
|
319
|
+
/**
|
|
320
|
+
* Sanitizes text for safe use in HTML attributes.
|
|
321
|
+
* Escapes characters that could break out of attribute context or inject code.
|
|
322
|
+
*
|
|
323
|
+
* Escapes: &, ", ', <, >, newlines, carriage returns
|
|
324
|
+
*
|
|
325
|
+
* @param text - The text to sanitize
|
|
326
|
+
* @returns HTML-safe text for use in attributes
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* HtmlSanitizer.sanitizeForAttribute('value with "quotes"');
|
|
330
|
+
* // Returns: 'value with "quotes"'
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* const value = HtmlSanitizer.sanitizeForAttribute(userInput);
|
|
334
|
+
* // Safe in: <div data-value="${value}">...</div>
|
|
335
|
+
*/
|
|
336
|
+
static sanitizeForAttribute(text) {
|
|
337
|
+
if (!text || typeof text !== 'string') {
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
// DoS protection
|
|
341
|
+
if (text.length > MAX_INPUT_SIZE) {
|
|
342
|
+
logger.error('Attribute value exceeds maximum size, truncating', {
|
|
343
|
+
code: 'SANITIZER_ATTRIBUTE_TOO_LARGE',
|
|
344
|
+
source: 'HtmlSanitizer.sanitizeForAttribute',
|
|
345
|
+
size: text.length,
|
|
346
|
+
max: MAX_INPUT_SIZE,
|
|
347
|
+
});
|
|
348
|
+
text = text.substring(0, MAX_INPUT_SIZE);
|
|
349
|
+
}
|
|
350
|
+
// Order matters: & first to avoid double-escaping
|
|
351
|
+
return text
|
|
352
|
+
.replace(/&/g, '&')
|
|
353
|
+
.replace(/"/g, '"')
|
|
354
|
+
.replace(/'/g, ''')
|
|
355
|
+
.replace(/</g, '<')
|
|
356
|
+
.replace(/>/g, '>')
|
|
357
|
+
.replace(/\n/g, ' ') // Newlines could break attributes
|
|
358
|
+
.replace(/\r/g, ' '); // Carriage returns
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Sanitizes text for safe display in HTML content.
|
|
362
|
+
* Escapes HTML special characters to prevent XSS attacks.
|
|
363
|
+
*
|
|
364
|
+
* Escapes: &, <, >, ", '
|
|
365
|
+
*
|
|
366
|
+
* @param text - The text to sanitize
|
|
367
|
+
* @returns HTML-safe text that can be inserted into HTML content
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* HtmlSanitizer.sanitizeForHtml('<script>alert("xss")</script>');
|
|
371
|
+
* // Returns: '<script>alert("xss")</script>'
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* const safeText = HtmlSanitizer.sanitizeForHtml(userInput);
|
|
375
|
+
* // Safe in: <p>${safeText}</p>
|
|
376
|
+
*/
|
|
377
|
+
static sanitizeForHtml(text) {
|
|
378
|
+
if (!text || typeof text !== 'string') {
|
|
379
|
+
return '';
|
|
380
|
+
}
|
|
381
|
+
// DoS protection
|
|
382
|
+
if (text.length > MAX_INPUT_SIZE) {
|
|
383
|
+
logger.error('HTML content exceeds maximum size, truncating', {
|
|
384
|
+
code: 'SANITIZER_HTML_TOO_LARGE',
|
|
385
|
+
source: 'HtmlSanitizer.sanitizeForHtml',
|
|
386
|
+
size: text.length,
|
|
387
|
+
max: MAX_INPUT_SIZE,
|
|
388
|
+
});
|
|
389
|
+
text = text.substring(0, MAX_INPUT_SIZE);
|
|
390
|
+
}
|
|
391
|
+
// Order matters: & first to avoid double-escaping
|
|
392
|
+
return text
|
|
393
|
+
.replace(/&/g, '&')
|
|
394
|
+
.replace(/</g, '<')
|
|
395
|
+
.replace(/>/g, '>')
|
|
396
|
+
.replace(/"/g, '"')
|
|
397
|
+
.replace(/'/g, ''');
|
|
398
|
+
}
|
|
399
|
+
// ========================================================================
|
|
400
|
+
// URL Validation
|
|
401
|
+
// ========================================================================
|
|
402
|
+
/**
|
|
403
|
+
* Validates if a URL is safe for use in links.
|
|
404
|
+
* Blocks dangerous protocols and validates against common XSS vectors.
|
|
405
|
+
*
|
|
406
|
+
* Blocked protocols: javascript:, vbscript:, data:, file:, about:, blob:
|
|
407
|
+
* Allowed protocols: https://, http://, mailto:, tel:, sms:, relative paths
|
|
408
|
+
*
|
|
409
|
+
* @param url - The URL to validate
|
|
410
|
+
* @returns True if the URL is considered safe, false otherwise
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* HtmlSanitizer.isValidUrl('https://example.com');
|
|
414
|
+
* // Returns: true
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* HtmlSanitizer.isValidUrl('javascript:alert(1)');
|
|
418
|
+
* // Returns: false
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* HtmlSanitizer.isValidUrl('/relative/path');
|
|
422
|
+
* // Returns: true
|
|
423
|
+
*/
|
|
424
|
+
static isValidUrl(url) {
|
|
425
|
+
if (!url || typeof url !== 'string') {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
// Remove any leading/trailing whitespace
|
|
429
|
+
url = url.trim();
|
|
430
|
+
// Empty URLs are not valid
|
|
431
|
+
if (url.length === 0) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
// DoS protection - reject excessively long URLs
|
|
435
|
+
if (url.length > MAX_URL_LENGTH) {
|
|
436
|
+
logger.warn('URL exceeds maximum length', {
|
|
437
|
+
code: 'SANITIZER_URL_TOO_LONG',
|
|
438
|
+
source: 'HtmlSanitizer.isValidUrl',
|
|
439
|
+
length: url.length,
|
|
440
|
+
max: MAX_URL_LENGTH,
|
|
441
|
+
});
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
// Normalize to lowercase for protocol checking
|
|
445
|
+
const urlLower = url.toLowerCase();
|
|
446
|
+
// Check for dangerous protocols first (XSS vectors)
|
|
447
|
+
if (DANGEROUS_PROTOCOLS.test(urlLower)) {
|
|
448
|
+
logger.warn('Dangerous protocol detected in URL', {
|
|
449
|
+
code: 'SANITIZER_DANGEROUS_PROTOCOL',
|
|
450
|
+
source: 'HtmlSanitizer.isValidUrl',
|
|
451
|
+
url,
|
|
452
|
+
});
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
// Allow safe protocols
|
|
456
|
+
// If it has a protocol, it must be from the safe list
|
|
457
|
+
if (url.includes(':')) {
|
|
458
|
+
const isValid = SAFE_PROTOCOLS.test(url);
|
|
459
|
+
if (!isValid) {
|
|
460
|
+
logger.warn('Unknown/unsafe protocol in URL', {
|
|
461
|
+
code: 'SANITIZER_UNSAFE_PROTOCOL',
|
|
462
|
+
source: 'HtmlSanitizer.isValidUrl',
|
|
463
|
+
url,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return isValid;
|
|
467
|
+
}
|
|
468
|
+
// Relative URLs without protocols are generally safe,
|
|
469
|
+
// but check for encoded dangerous protocols
|
|
470
|
+
if (url.includes('%')) {
|
|
471
|
+
try {
|
|
472
|
+
const decoded = decodeURIComponent(url);
|
|
473
|
+
if (DANGEROUS_PROTOCOLS.test(decoded)) {
|
|
474
|
+
logger.warn('Encoded dangerous protocol detected', {
|
|
475
|
+
code: 'SANITIZER_ENCODED_DANGEROUS_PROTOCOL',
|
|
476
|
+
source: 'HtmlSanitizer.isValidUrl',
|
|
477
|
+
url,
|
|
478
|
+
});
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
// If decoding fails, reject to be safe
|
|
484
|
+
logger.warn('Failed to decode URL - rejecting', {
|
|
485
|
+
code: 'SANITIZER_URL_DECODE_ERROR',
|
|
486
|
+
source: 'HtmlSanitizer.isValidUrl',
|
|
487
|
+
url,
|
|
488
|
+
error: e,
|
|
489
|
+
});
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Validates if an email address is properly formatted.
|
|
497
|
+
* Performs comprehensive email validation following RFC 5321/5322 standards.
|
|
498
|
+
*
|
|
499
|
+
* Validation Rules:
|
|
500
|
+
* - Maximum length: 254 characters (RFC 5321)
|
|
501
|
+
* - Local part (before @): 1-64 characters, allows a-z, 0-9, . _ + % -
|
|
502
|
+
* - Local part cannot start/end with dot or contain consecutive dots
|
|
503
|
+
* - Domain part (after @): 1-255 characters, requires valid TLD
|
|
504
|
+
* - Domain labels: 1-63 characters each, allows a-z, 0-9, hyphen (not at start/end)
|
|
505
|
+
* - Must contain exactly one @ symbol
|
|
506
|
+
* - Case-insensitive validation
|
|
507
|
+
*
|
|
508
|
+
* Security Features:
|
|
509
|
+
* - DoS protection with length limits
|
|
510
|
+
* - Rejects malformed patterns that could be used for injection
|
|
511
|
+
* - Validates against common email spoofing patterns
|
|
512
|
+
*
|
|
513
|
+
* Note: This performs format validation only. For production use, consider:
|
|
514
|
+
* - DNS MX record verification
|
|
515
|
+
* - Email verification (send confirmation email)
|
|
516
|
+
* - Third-party email validation services
|
|
517
|
+
* - Disposable email detection
|
|
518
|
+
*
|
|
519
|
+
* @param email - The email address to validate
|
|
520
|
+
* @returns True if email format is valid, false otherwise
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* HtmlSanitizer.isValidEmail('user@example.com');
|
|
524
|
+
* // Returns: true
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* HtmlSanitizer.isValidEmail('user+tag@subdomain.example.co.uk');
|
|
528
|
+
* // Returns: true
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* HtmlSanitizer.isValidEmail('invalid.email');
|
|
532
|
+
* // Returns: false
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* HtmlSanitizer.isValidEmail('user..name@example.com');
|
|
536
|
+
* // Returns: false (consecutive dots)
|
|
537
|
+
*/
|
|
538
|
+
static isValidEmail(email) {
|
|
539
|
+
// ====================================================================
|
|
540
|
+
// Step 1: Basic Input Validation
|
|
541
|
+
// ====================================================================
|
|
542
|
+
if (!email || typeof email !== 'string') {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
// Trim whitespace
|
|
546
|
+
email = email.trim();
|
|
547
|
+
if (email.length === 0) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
// DoS protection - RFC 5321 maximum is 254 characters
|
|
551
|
+
const MAX_EMAIL_LENGTH = 254;
|
|
552
|
+
if (email.length > MAX_EMAIL_LENGTH) {
|
|
553
|
+
logger.warn('Email exceeds maximum length', {
|
|
554
|
+
code: 'SANITIZER_EMAIL_TOO_LONG',
|
|
555
|
+
source: 'HtmlSanitizer.isValidEmail',
|
|
556
|
+
length: email.length,
|
|
557
|
+
max: MAX_EMAIL_LENGTH,
|
|
558
|
+
});
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
// ====================================================================
|
|
562
|
+
// Step 2: Split and Validate Structure
|
|
563
|
+
// ====================================================================
|
|
564
|
+
// Must contain exactly one @ symbol
|
|
565
|
+
const atIndex = email.indexOf('@');
|
|
566
|
+
if (atIndex === -1 || atIndex !== email.lastIndexOf('@')) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
const localPart = email.substring(0, atIndex);
|
|
570
|
+
const domainPart = email.substring(atIndex + 1);
|
|
571
|
+
// ====================================================================
|
|
572
|
+
// Step 3: Validate Local Part (before @)
|
|
573
|
+
// ====================================================================
|
|
574
|
+
// RFC 5321: local part max 64 characters
|
|
575
|
+
if (localPart.length === 0 || localPart.length > 64) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
// Local part cannot start or end with a dot
|
|
579
|
+
if (localPart.startsWith('.') || localPart.endsWith('.')) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
// Local part cannot contain consecutive dots
|
|
583
|
+
if (localPart.includes('..')) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
// Validate local part characters: a-z, 0-9, . _ + % -
|
|
587
|
+
if (!/^[a-zA-Z0-9._+%-]+$/.test(localPart)) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
// ====================================================================
|
|
591
|
+
// Step 4: Validate Domain Part (after @)
|
|
592
|
+
// ====================================================================
|
|
593
|
+
// RFC 5321: domain part max 255 characters
|
|
594
|
+
if (domainPart.length === 0 || domainPart.length > 255) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
// Domain must contain at least one dot (TLD required)
|
|
598
|
+
if (!domainPart.includes('.')) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
// Domain cannot start or end with dot or hyphen
|
|
602
|
+
if (domainPart.startsWith('.') ||
|
|
603
|
+
domainPart.endsWith('.') ||
|
|
604
|
+
domainPart.startsWith('-') ||
|
|
605
|
+
domainPart.endsWith('-')) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
// Domain cannot contain consecutive dots
|
|
609
|
+
if (domainPart.includes('..')) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
// ====================================================================
|
|
613
|
+
// Step 5: Validate Domain Labels
|
|
614
|
+
// ====================================================================
|
|
615
|
+
const domainLabels = domainPart.split('.');
|
|
616
|
+
// Must have at least 2 labels (domain + TLD)
|
|
617
|
+
if (domainLabels.length < 2) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
for (const label of domainLabels) {
|
|
621
|
+
// Each label: 1-63 characters (RFC 1035)
|
|
622
|
+
if (label.length === 0 || label.length > 63) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
// Labels cannot start or end with hyphen
|
|
626
|
+
if (label.startsWith('-') || label.endsWith('-')) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
// Labels can only contain: a-z, 0-9, hyphen
|
|
630
|
+
if (!/^[a-zA-Z0-9-]+$/.test(label)) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// Validate TLD (last label) - must be at least 2 characters and alphabetic
|
|
635
|
+
const tld = domainLabels[domainLabels.length - 1];
|
|
636
|
+
if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
// ========================================================================
|
|
642
|
+
// Entity Decoding
|
|
643
|
+
// ========================================================================
|
|
644
|
+
/**
|
|
645
|
+
* Decodes common HTML entities to their character equivalents.
|
|
646
|
+
* Safely handles both named entities and numeric character references.
|
|
647
|
+
*
|
|
648
|
+
* Features:
|
|
649
|
+
* - Decodes common named entities (&, <, , etc.)
|
|
650
|
+
* - Decodes numeric entities (A and A both -> 'A')
|
|
651
|
+
* - Validates character codes are in valid Unicode range
|
|
652
|
+
* - Excludes control characters (except tab, newline, CR)
|
|
653
|
+
*
|
|
654
|
+
* @param text - Text containing HTML entities
|
|
655
|
+
* @returns Text with entities decoded to characters
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* HtmlSanitizer.decodeHtmlEntities('<p>Hello & goodbye</p>');
|
|
659
|
+
* // Returns: '<p>Hello & goodbye</p>'
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* HtmlSanitizer.decodeHtmlEntities('ABC'); // ABC
|
|
663
|
+
* HtmlSanitizer.decodeHtmlEntities('ABC'); // ABC
|
|
664
|
+
*/
|
|
665
|
+
static decodeHtmlEntities(text) {
|
|
666
|
+
if (!text || typeof text !== 'string') {
|
|
667
|
+
return '';
|
|
668
|
+
}
|
|
669
|
+
// DoS protection
|
|
670
|
+
if (text.length > MAX_INPUT_SIZE) {
|
|
671
|
+
logger.error('Input exceeds maximum size for entity decoding', {
|
|
672
|
+
code: 'SANITIZER_ENTITY_INPUT_TOO_LARGE',
|
|
673
|
+
source: 'HtmlSanitizer.decodeHtmlEntities',
|
|
674
|
+
size: text.length,
|
|
675
|
+
max: MAX_INPUT_SIZE,
|
|
676
|
+
});
|
|
677
|
+
text = text.substring(0, MAX_INPUT_SIZE);
|
|
678
|
+
}
|
|
679
|
+
// Common HTML entities to decode
|
|
680
|
+
const entities = {
|
|
681
|
+
'&': '&',
|
|
682
|
+
'<': '<',
|
|
683
|
+
'>': '>',
|
|
684
|
+
'"': '"',
|
|
685
|
+
''': "'",
|
|
686
|
+
''': "'",
|
|
687
|
+
' ': ' ',
|
|
688
|
+
'©': '©',
|
|
689
|
+
'®': '®',
|
|
690
|
+
'™': '™',
|
|
691
|
+
};
|
|
692
|
+
// Replace known named entities
|
|
693
|
+
for (const [entity, char] of Object.entries(entities)) {
|
|
694
|
+
text = text.replace(new RegExp(entity, 'g'), char);
|
|
695
|
+
}
|
|
696
|
+
// Handle numeric entities (decimal) - with safety limits
|
|
697
|
+
text = text.replace(/&#(\d+);/g, (match, dec) => {
|
|
698
|
+
const code = parseInt(dec, 10);
|
|
699
|
+
// Limit to valid Unicode range and exclude control characters (except tab, newline, carriage return)
|
|
700
|
+
if (code > 0 &&
|
|
701
|
+
code <= 0x10ffff &&
|
|
702
|
+
(code >= 32 || code === 9 || code === 10 || code === 13)) {
|
|
703
|
+
try {
|
|
704
|
+
return String.fromCharCode(code);
|
|
705
|
+
}
|
|
706
|
+
catch (e) {
|
|
707
|
+
logger.warn('Invalid character code', {
|
|
708
|
+
code: 'SANITIZER_INVALID_CHAR_CODE',
|
|
709
|
+
source: 'HtmlSanitizer.decodeHtmlEntities',
|
|
710
|
+
charCode: code,
|
|
711
|
+
error: e,
|
|
712
|
+
});
|
|
713
|
+
return match;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return match;
|
|
717
|
+
});
|
|
718
|
+
// Handle numeric entities (hexadecimal) - with safety limits
|
|
719
|
+
text = text.replace(/&#x([0-9a-f]+);/gi, (match, hex) => {
|
|
720
|
+
const code = parseInt(hex, 16);
|
|
721
|
+
// Limit to valid Unicode range and exclude control characters (except tab, newline, carriage return)
|
|
722
|
+
if (code > 0 &&
|
|
723
|
+
code <= 0x10ffff &&
|
|
724
|
+
(code >= 32 || code === 9 || code === 10 || code === 13)) {
|
|
725
|
+
try {
|
|
726
|
+
return String.fromCharCode(code);
|
|
727
|
+
}
|
|
728
|
+
catch (e) {
|
|
729
|
+
logger.warn('Invalid hex character code', {
|
|
730
|
+
code: 'SANITIZER_INVALID_HEX_CODE',
|
|
731
|
+
source: 'HtmlSanitizer.decodeHtmlEntities',
|
|
732
|
+
hex,
|
|
733
|
+
error: e,
|
|
734
|
+
});
|
|
735
|
+
return match;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return match;
|
|
739
|
+
});
|
|
740
|
+
return text;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// ============================================================================
|
|
744
|
+
// Usage Guidelines & Best Practices
|
|
745
|
+
// ============================================================================
|
|
746
|
+
/**
|
|
747
|
+
* USAGE GUIDELINES
|
|
748
|
+
*
|
|
749
|
+
* 1. USER-GENERATED CONTENT
|
|
750
|
+
* Always sanitize user input before storing or displaying:
|
|
751
|
+
* ```typescript
|
|
752
|
+
* // For rich text with limited formatting
|
|
753
|
+
* const sanitized = HtmlSanitizer.clean(userInput, ['p', 'br', 'strong', 'em', 'a']);
|
|
754
|
+
*
|
|
755
|
+
* // For plain text display
|
|
756
|
+
* const plainText = HtmlSanitizer.stripAll(userInput);
|
|
757
|
+
* ```
|
|
758
|
+
*
|
|
759
|
+
* 2. ATTRIBUTE VALUES
|
|
760
|
+
* Always escape text used in HTML attributes:
|
|
761
|
+
* ```typescript
|
|
762
|
+
* const safe = HtmlSanitizer.sanitizeForAttribute(userInput);
|
|
763
|
+
* html = `<div data-value="${safe}">...</div>`;
|
|
764
|
+
* ```
|
|
765
|
+
*
|
|
766
|
+
* 3. HTML CONTENT
|
|
767
|
+
* Escape text to be displayed as-is in HTML:
|
|
768
|
+
* ```typescript
|
|
769
|
+
* const safe = HtmlSanitizer.sanitizeForHtml(userInput);
|
|
770
|
+
* html = `<p>${safe}</p>`;
|
|
771
|
+
* ```
|
|
772
|
+
*
|
|
773
|
+
* 4. ALLOWED TAGS
|
|
774
|
+
* Choose the minimum set of tags needed:
|
|
775
|
+
* - Basic formatting: ['b', 'i', 'u', 'br']
|
|
776
|
+
* - Rich text: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li']
|
|
777
|
+
* - Extended: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3']
|
|
778
|
+
*
|
|
779
|
+
* 5. URL VALIDATION
|
|
780
|
+
* URLs are automatically validated in anchor tags:
|
|
781
|
+
* - Allowed: https://, http://, mailto:, tel:, sms:, relative paths
|
|
782
|
+
* - Blocked: javascript:, data:, vbscript:, file:, blob:, about:
|
|
783
|
+
*
|
|
784
|
+
* 6. PERFORMANCE
|
|
785
|
+
* - Input size limited to 1MB (DoS protection)
|
|
786
|
+
* - Use stripAll() for better performance when HTML not needed
|
|
787
|
+
* - Cache sanitized content when possible
|
|
788
|
+
*
|
|
789
|
+
* SECURITY NOTES
|
|
790
|
+
*
|
|
791
|
+
* - XSS Prevention: All dangerous protocols and scripts are blocked
|
|
792
|
+
* - DoS Protection: Input size limits prevent resource exhaustion
|
|
793
|
+
* - Context-Aware: Different methods for different contexts (attributes vs content)
|
|
794
|
+
* - Defense in Depth: Multiple layers of validation and sanitization
|
|
795
|
+
* - Logging: Security events are logged to console for monitoring
|
|
796
|
+
*
|
|
797
|
+
* LIMITATIONS
|
|
798
|
+
*
|
|
799
|
+
* - Does not validate HTML structure (unclosed tags, nesting rules)
|
|
800
|
+
* - Does not support CSS sanitization
|
|
801
|
+
* - Does not support SVG sanitization
|
|
802
|
+
* - Maximum input size: 1MB
|
|
803
|
+
* - Maximum URL length: 2048 characters
|
|
804
|
+
*
|
|
805
|
+
* @see https://owasp.org/www-community/attacks/xss/
|
|
806
|
+
* @see https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
|
|
807
|
+
*/
|
|
808
|
+
//# sourceMappingURL=HtmlSanitizer.js.map
|