@devilsdev/rag-pipeline-utils 2.2.0 → 2.3.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.
Files changed (46) hide show
  1. package/README.md +323 -722
  2. package/contracts/contract-schema.json +189 -0
  3. package/contracts/reranker-contract.json +69 -0
  4. package/dist/index.cjs +19 -0
  5. package/dist/index.mjs +15 -1
  6. package/package.json +4 -2
  7. package/src/cli/commands/wizard.js +627 -0
  8. package/src/cli/input-sanitization-middleware.js +356 -0
  9. package/src/cli/templates/development.template.json +64 -0
  10. package/src/cli/templates/minimal.template.json +33 -0
  11. package/src/cli/templates/production.template.json +104 -0
  12. package/src/cli/templates/testing.template.json +45 -0
  13. package/src/config/connection-pool-config.json +259 -0
  14. package/src/config/memory-optimization.js +414 -0
  15. package/src/config/redaction-patterns.json +150 -0
  16. package/src/config/sanitization-rules.json +323 -0
  17. package/src/core/performance/streaming-safeguards.js +10 -1
  18. package/src/core/plugin-registry.js +275 -0
  19. package/src/dag/core/error-context.js +391 -0
  20. package/src/dag/core/execution-scheduler.js +350 -0
  21. package/src/dag/core/topology-validation.js +415 -0
  22. package/src/dag/dag-engine.js +26 -424
  23. package/src/dev/README.md +126 -0
  24. package/src/dev/dev-server.js +356 -0
  25. package/src/dev/examples/hot-reload-usage.js +355 -0
  26. package/src/dev/hot-reload.js +807 -0
  27. package/src/enterprise/sso-integration.js +184 -15
  28. package/src/index.js +19 -0
  29. package/src/observability/event-logger.js +62 -2
  30. package/src/plugins/optimized-openai-embedder.js +169 -0
  31. package/src/reranker/llm-reranker.js +30 -4
  32. package/src/security/jwt-config.js +431 -0
  33. package/src/security/jwt-validator.js +701 -0
  34. package/src/utils/api-client-pool-integration.js +436 -0
  35. package/src/utils/batch-processor.js +710 -0
  36. package/src/utils/connection-pool.js +605 -0
  37. package/src/utils/contract-validator.js +460 -0
  38. package/src/utils/error-formatter.js +603 -0
  39. package/src/utils/examples/error-formatter-usage.js +351 -0
  40. package/src/utils/input-sanitizer.js +967 -0
  41. package/src/utils/memory-monitor.js +800 -0
  42. package/src/utils/rate-limiter.js +244 -0
  43. package/src/utils/secure-logger-config.js +352 -0
  44. package/src/utils/secure-logger.js +576 -0
  45. package/src/utils/secure-logging.js +338 -0
  46. package/src/utils/structured-logger.js +32 -5
@@ -0,0 +1,967 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Input Sanitizer
5
+ *
6
+ * Comprehensive input validation and sanitization utility to prevent:
7
+ * - Cross-Site Scripting (XSS) attacks
8
+ * - SQL Injection attacks
9
+ * - Command Injection attacks
10
+ * - Path Traversal attacks
11
+ * - NoSQL Injection attacks
12
+ * - LDAP Injection attacks
13
+ *
14
+ * Performance optimized for high-throughput scenarios with caching
15
+ * and efficient regex patterns.
16
+ *
17
+ * @module utils/input-sanitizer
18
+ * @since 2.3.0
19
+ */
20
+
21
+ /**
22
+ * Sanitization rules enum
23
+ */
24
+ const SanitizationRules = {
25
+ NONE: 'none', // No sanitization
26
+ HTML: 'html', // HTML escape
27
+ HTML_STRICT: 'html_strict', // Remove all HTML tags
28
+ XSS: 'xss', // XSS prevention (aggressive)
29
+ SQL: 'sql', // SQL injection prevention
30
+ COMMAND: 'command', // Command injection prevention
31
+ PATH: 'path', // Path traversal prevention
32
+ URL: 'url', // URL validation and sanitization
33
+ EMAIL: 'email', // Email validation
34
+ ALPHANUMERIC: 'alphanumeric', // Only letters and numbers
35
+ UUID: 'uuid', // UUID validation
36
+ INTEGER: 'integer', // Integer validation
37
+ FLOAT: 'float', // Float validation
38
+ BOOLEAN: 'boolean', // Boolean validation
39
+ JSON: 'json', // JSON validation
40
+ BASE64: 'base64', // Base64 validation
41
+ HEX: 'hex', // Hexadecimal validation
42
+ SLUG: 'slug', // Slug format (URL-friendly)
43
+ DOMAIN: 'domain', // Domain name validation
44
+ IPV4: 'ipv4', // IPv4 address validation
45
+ IPV6: 'ipv6', // IPv6 address validation
46
+ };
47
+
48
+ /**
49
+ * Common validation patterns (compiled for performance)
50
+ */
51
+ const ValidationPatterns = {
52
+ email:
53
+ /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
54
+ uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
55
+ url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$/,
56
+ slug: /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
57
+ domain:
58
+ /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i,
59
+ ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
60
+ ipv6: /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/,
61
+ alphanumeric: /^[a-zA-Z0-9]+$/,
62
+ integer: /^-?\d+$/,
63
+ float: /^-?\d+(\.\d+)?$/,
64
+ base64: /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/,
65
+ hex: /^[0-9a-fA-F]+$/,
66
+ };
67
+
68
+ /**
69
+ * Dangerous patterns that indicate attacks (compiled for performance)
70
+ */
71
+ const DangerousPatterns = {
72
+ // XSS patterns
73
+ xss: [
74
+ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
75
+ /javascript:/gi,
76
+ /on\w+\s*=/gi, // Event handlers: onclick=, onload=, etc.
77
+ /<iframe/gi,
78
+ /<object/gi,
79
+ /<embed/gi,
80
+ /<applet/gi,
81
+ /eval\s*\(/gi,
82
+ /expression\s*\(/gi,
83
+ /<meta/gi,
84
+ /<link/gi,
85
+ /alert\s*\(/gi, // Remove alert() function calls
86
+ /confirm\s*\(/gi, // Remove confirm() function calls
87
+ /prompt\s*\(/gi, // Remove prompt() function calls
88
+ ],
89
+
90
+ // SQL injection patterns
91
+ sql: [
92
+ /(\bor\b|\band\b)\s+['"]?\d+['"]?\s*=\s*['"]?\d+/gi,
93
+ /union\s+select/gi,
94
+ /;\s*(drop|delete|insert|update|create|alter|exec|execute)\s+/gi,
95
+ /xp_cmdshell/gi,
96
+ /waitfor\s+delay/gi,
97
+ /benchmark\s*\(/gi,
98
+ ],
99
+
100
+ // Command injection patterns
101
+ command: [
102
+ /[;&|`$()]/g,
103
+ /\$\{[^}]+\}/g, // Template literals
104
+ /\$\([^)]+\)/g, // Command substitution
105
+ />\s*\/dev\//gi,
106
+ /\|\s*tee\b/gi,
107
+ ],
108
+
109
+ // Path traversal patterns
110
+ pathTraversal: [
111
+ /\.\.[/\\]/g,
112
+ /\.\.[/\\]\.\./g,
113
+ /\/\.\.\//g,
114
+ /\\\.\.\\/g,
115
+ /%2e%2e[/\\]/gi,
116
+ /%252e%252e/gi,
117
+ ],
118
+
119
+ // NoSQL injection patterns
120
+ nosql: [
121
+ /\$where/gi,
122
+ /\$ne\b/gi,
123
+ /\$gt\b/gi,
124
+ /\$lt\b/gi,
125
+ /\$or\b/gi,
126
+ /\$and\b/gi,
127
+ ],
128
+ };
129
+
130
+ /**
131
+ * HTML entity map for escaping
132
+ */
133
+ const HtmlEntityMap = {
134
+ '&': '&amp;',
135
+ '<': '&lt;',
136
+ '>': '&gt;',
137
+ '"': '&quot;',
138
+ '\'': '&#x27;',
139
+ '/': '&#x2F;',
140
+ };
141
+
142
+ /**
143
+ * Input Sanitizer Class
144
+ */
145
+ class InputSanitizer {
146
+ constructor(options = {}) {
147
+ this.options = {
148
+ // Performance optimization
149
+ enableCache: options.enableCache !== false,
150
+ cacheSize: options.cacheSize || 1000,
151
+ cacheTTL: options.cacheTTL || 60000, // 1 minute
152
+
153
+ // Default rules
154
+ defaultStringRule: options.defaultStringRule || SanitizationRules.HTML,
155
+ defaultObjectDepth: options.defaultObjectDepth || 10,
156
+
157
+ // Strictness
158
+ throwOnInvalid: options.throwOnInvalid || false,
159
+ stripInvalid: options.stripInvalid !== false,
160
+
161
+ // Custom patterns
162
+ customPatterns: options.customPatterns || {},
163
+ customRules: options.customRules || {},
164
+
165
+ // Statistics
166
+ trackStats: options.trackStats !== false,
167
+ };
168
+
169
+ // Initialize cache
170
+ this.cache = new Map();
171
+ this.cacheAccessTimes = new Map();
172
+
173
+ // Statistics
174
+ this.stats = {
175
+ sanitized: 0,
176
+ cached: 0,
177
+ blocked: 0,
178
+ validationErrors: 0,
179
+ averageTime: 0,
180
+ totalTime: 0,
181
+ };
182
+
183
+ // Start cache cleanup if enabled
184
+ if (this.options.enableCache) {
185
+ this._startCacheCleanup();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Main sanitization function
191
+ *
192
+ * @param {*} input - Input to sanitize
193
+ * @param {string|Object} rules - Sanitization rules or config
194
+ * @returns {*} Sanitized input
195
+ *
196
+ * @example
197
+ * // Simple sanitization
198
+ * const safe = sanitizer.sanitize('<script>alert("xss")</script>', 'html');
199
+ *
200
+ * // With rules object
201
+ * const safe = sanitizer.sanitize({
202
+ * username: '<script>hack</script>',
203
+ * email: 'user@example.com',
204
+ * age: '25'
205
+ * }, {
206
+ * username: 'xss',
207
+ * email: 'email',
208
+ * age: 'integer'
209
+ * });
210
+ */
211
+ sanitize(input, rules = null) {
212
+ // Use high-resolution timing
213
+ let startTime;
214
+ if (typeof process !== 'undefined' && process.hrtime) {
215
+ startTime = process.hrtime.bigint();
216
+ } else if (
217
+ typeof performance !== 'undefined' &&
218
+ typeof performance.now === 'function'
219
+ ) {
220
+ startTime = performance.now();
221
+ } else {
222
+ startTime = Date.now();
223
+ }
224
+
225
+ try {
226
+ // Handle null/undefined
227
+ if (input === null || input === undefined) {
228
+ return input;
229
+ }
230
+
231
+ // Determine sanitization strategy
232
+ let result;
233
+
234
+ if (typeof input === 'string') {
235
+ result = this._sanitizeString(
236
+ input,
237
+ rules || this.options.defaultStringRule,
238
+ );
239
+ } else if (Array.isArray(input)) {
240
+ result = this._sanitizeArray(input, rules);
241
+ } else if (typeof input === 'object') {
242
+ result = this._sanitizeObject(input, rules, 0);
243
+ } else {
244
+ // Primitives (number, boolean, etc.)
245
+ result = input;
246
+ }
247
+
248
+ // Update statistics
249
+ if (this.options.trackStats) {
250
+ let duration;
251
+ if (
252
+ typeof process !== 'undefined' &&
253
+ process.hrtime &&
254
+ process.hrtime.bigint
255
+ ) {
256
+ try {
257
+ const endTime = process.hrtime.bigint();
258
+ // Convert nanoseconds to milliseconds
259
+ duration = Number(endTime - startTime) / 1000000;
260
+ } catch (e) {
261
+ // Fallback if hrtime.bigint() fails (e.g., in test environments with fake timers)
262
+ duration = 0.001; // Minimum 1 microsecond
263
+ }
264
+ } else if (
265
+ typeof performance !== 'undefined' &&
266
+ typeof performance.now === 'function'
267
+ ) {
268
+ duration = performance.now() - startTime;
269
+ } else {
270
+ duration = Date.now() - startTime;
271
+ }
272
+
273
+ // Ensure duration is at least 0.001ms (1 microsecond) to avoid zero averages
274
+ if (duration === 0 || !isFinite(duration)) {
275
+ duration = 0.001;
276
+ }
277
+
278
+ this.stats.sanitized++;
279
+ this.stats.totalTime += duration;
280
+ this.stats.averageTime = this.stats.totalTime / this.stats.sanitized;
281
+ }
282
+
283
+ return result;
284
+ } catch (error) {
285
+ // Always throw critical security errors regardless of throwOnInvalid setting
286
+ if (error.message) {
287
+ // Path traversal is a critical security violation - always throw
288
+ if (error.message.includes('Potential path traversal detected')) {
289
+ throw error;
290
+ }
291
+ // Depth errors should also always throw
292
+ if (error.message.includes('Maximum object depth exceeded')) {
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ if (this.options.throwOnInvalid) {
298
+ throw error;
299
+ }
300
+
301
+ this.stats.validationErrors++;
302
+ return this.options.stripInvalid ? null : input;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Sanitize string input
308
+ * @private
309
+ */
310
+ _sanitizeString(str, rule) {
311
+ if (typeof str !== 'string') {
312
+ return str;
313
+ }
314
+
315
+ // Check cache
316
+ if (this.options.enableCache) {
317
+ const cacheKey = `${rule}:${str}`;
318
+ if (this.cache.has(cacheKey)) {
319
+ this.cacheAccessTimes.set(cacheKey, Date.now());
320
+ this.stats.cached++;
321
+ return this.cache.get(cacheKey);
322
+ }
323
+ }
324
+
325
+ let result;
326
+
327
+ switch (rule) {
328
+ case SanitizationRules.NONE:
329
+ result = str;
330
+ break;
331
+
332
+ case SanitizationRules.HTML:
333
+ result = this._escapeHtml(str);
334
+ break;
335
+
336
+ case SanitizationRules.HTML_STRICT:
337
+ result = this._stripHtml(str);
338
+ break;
339
+
340
+ case SanitizationRules.XSS:
341
+ result = this._sanitizeXss(str);
342
+ break;
343
+
344
+ case SanitizationRules.SQL:
345
+ result = this._sanitizeSql(str);
346
+ break;
347
+
348
+ case SanitizationRules.COMMAND:
349
+ result = this._sanitizeCommand(str);
350
+ break;
351
+
352
+ case SanitizationRules.PATH:
353
+ result = this._sanitizePath(str);
354
+ break;
355
+
356
+ case SanitizationRules.URL:
357
+ result = this._validateAndSanitizeUrl(str);
358
+ break;
359
+
360
+ case SanitizationRules.EMAIL:
361
+ result = this._validateEmail(str);
362
+ break;
363
+
364
+ case SanitizationRules.ALPHANUMERIC:
365
+ result = this._validateAlphanumeric(str);
366
+ break;
367
+
368
+ case SanitizationRules.UUID:
369
+ result = this._validateUuid(str);
370
+ break;
371
+
372
+ case SanitizationRules.INTEGER:
373
+ result = this._validateInteger(str);
374
+ break;
375
+
376
+ case SanitizationRules.FLOAT:
377
+ result = this._validateFloat(str);
378
+ break;
379
+
380
+ case SanitizationRules.BOOLEAN:
381
+ result = this._validateBoolean(str);
382
+ break;
383
+
384
+ case SanitizationRules.JSON:
385
+ result = this._validateJson(str);
386
+ break;
387
+
388
+ case SanitizationRules.BASE64:
389
+ result = this._validateBase64(str);
390
+ break;
391
+
392
+ case SanitizationRules.HEX:
393
+ result = this._validateHex(str);
394
+ break;
395
+
396
+ case SanitizationRules.SLUG:
397
+ result = this._validateSlug(str);
398
+ break;
399
+
400
+ case SanitizationRules.DOMAIN:
401
+ result = this._validateDomain(str);
402
+ break;
403
+
404
+ case SanitizationRules.IPV4:
405
+ result = this._validateIpv4(str);
406
+ break;
407
+
408
+ case SanitizationRules.IPV6:
409
+ result = this._validateIpv6(str);
410
+ break;
411
+
412
+ default:
413
+ // Check custom rules
414
+ if (this.options.customRules[rule]) {
415
+ result = this.options.customRules[rule](str);
416
+ } else {
417
+ result = this._escapeHtml(str); // Default to HTML escape
418
+ }
419
+ }
420
+
421
+ // Cache result
422
+ if (this.options.enableCache) {
423
+ const cacheKey = `${rule}:${str}`;
424
+ this._addToCache(cacheKey, result);
425
+ }
426
+
427
+ return result;
428
+ }
429
+
430
+ /**
431
+ * Sanitize array
432
+ * @private
433
+ */
434
+ _sanitizeArray(arr, rules) {
435
+ return arr.map((item) => this.sanitize(item, rules));
436
+ }
437
+
438
+ /**
439
+ * Sanitize object recursively
440
+ * @private
441
+ */
442
+ _sanitizeObject(obj, rules, depth = 0) {
443
+ if (depth >= this.options.defaultObjectDepth) {
444
+ throw new Error(
445
+ `Maximum object depth exceeded: ${this.options.defaultObjectDepth}`,
446
+ );
447
+ }
448
+
449
+ const sanitized = {};
450
+
451
+ for (const [key, value] of Object.entries(obj)) {
452
+ // Sanitize key
453
+ const safeKey = this._sanitizeString(key, SanitizationRules.ALPHANUMERIC);
454
+
455
+ // Determine rule for this field
456
+ const fieldRule = rules && typeof rules === 'object' ? rules[key] : rules;
457
+
458
+ // Sanitize value
459
+ if (typeof value === 'string') {
460
+ sanitized[safeKey] = this._sanitizeString(value, fieldRule);
461
+ } else if (Array.isArray(value)) {
462
+ sanitized[safeKey] = this._sanitizeArray(value, fieldRule);
463
+ } else if (typeof value === 'object' && value !== null) {
464
+ sanitized[safeKey] = this._sanitizeObject(value, fieldRule, depth + 1);
465
+ } else {
466
+ sanitized[safeKey] = value;
467
+ }
468
+ }
469
+
470
+ return sanitized;
471
+ }
472
+
473
+ /**
474
+ * Escape HTML entities
475
+ * @private
476
+ */
477
+ _escapeHtml(str) {
478
+ return str.replace(/[&<>"'/]/g, (char) => HtmlEntityMap[char] || char);
479
+ }
480
+
481
+ /**
482
+ * Strip all HTML tags
483
+ * @private
484
+ */
485
+ _stripHtml(str) {
486
+ return str.replace(/<[^>]*>/g, '');
487
+ }
488
+
489
+ /**
490
+ * Aggressive XSS sanitization
491
+ * @private
492
+ */
493
+ _sanitizeXss(str) {
494
+ let sanitized = str;
495
+
496
+ // Apply all XSS patterns
497
+ for (const pattern of DangerousPatterns.xss) {
498
+ if (pattern.test(sanitized)) {
499
+ this.stats.blocked++;
500
+ sanitized = sanitized.replace(pattern, '');
501
+ }
502
+ }
503
+
504
+ // HTML escape remaining content
505
+ sanitized = this._escapeHtml(sanitized);
506
+
507
+ return sanitized;
508
+ }
509
+
510
+ /**
511
+ * SQL injection sanitization
512
+ * @private
513
+ */
514
+ _sanitizeSql(str) {
515
+ let sanitized = str;
516
+
517
+ // Check for SQL injection patterns
518
+ for (const pattern of DangerousPatterns.sql) {
519
+ if (pattern.test(sanitized)) {
520
+ this.stats.blocked++;
521
+ if (this.options.throwOnInvalid) {
522
+ throw new Error('Potential SQL injection detected');
523
+ }
524
+ // Remove dangerous patterns
525
+ sanitized = sanitized.replace(pattern, '');
526
+ }
527
+ }
528
+
529
+ // Escape single quotes (basic SQL escaping)
530
+ sanitized = sanitized.replace(/'/g, '\'\'');
531
+
532
+ return sanitized;
533
+ }
534
+
535
+ /**
536
+ * Command injection sanitization
537
+ * @private
538
+ */
539
+ _sanitizeCommand(str) {
540
+ // Check for command injection patterns
541
+ for (const pattern of DangerousPatterns.command) {
542
+ if (pattern.test(str)) {
543
+ this.stats.blocked++;
544
+ if (this.options.throwOnInvalid) {
545
+ throw new Error('Potential command injection detected');
546
+ }
547
+ return '';
548
+ }
549
+ }
550
+
551
+ return str;
552
+ }
553
+
554
+ /**
555
+ * Path traversal sanitization
556
+ * @private
557
+ */
558
+ _sanitizePath(str) {
559
+ // Iteratively decode URL encoding to catch double/triple encoded attacks
560
+ // e.g., %252e%252e%252f -> %2e%2e%2f -> ../
561
+ let current = str;
562
+ let previous = '';
563
+ let iterations = 0;
564
+ const MAX_DECODE_ITERATIONS = 5;
565
+
566
+ while (current !== previous && iterations < MAX_DECODE_ITERATIONS) {
567
+ previous = current;
568
+ try {
569
+ current = decodeURIComponent(current);
570
+ } catch (e) {
571
+ // Malformed URL encoding might indicate attack - log and block
572
+ this.stats.blocked++;
573
+ throw new Error(
574
+ 'Potential path traversal detected (malformed encoding)',
575
+ );
576
+ }
577
+ iterations++;
578
+ }
579
+
580
+ // Check for path traversal patterns in both original and fully decoded versions
581
+ for (const pattern of DangerousPatterns.pathTraversal) {
582
+ if (pattern.test(str) || pattern.test(current)) {
583
+ this.stats.blocked++;
584
+ // SECURITY: Always throw on path traversal - it should never be silently sanitized
585
+ // Empty paths could resolve to application root, creating security vulnerabilities
586
+ throw new Error('Potential path traversal detected');
587
+ }
588
+ }
589
+
590
+ // Remove leading/trailing slashes
591
+ current = current.replace(/^[/\\]+|[/\\]+$/g, '');
592
+
593
+ // Normalize path separators
594
+ current = current.replace(/\\/g, '/');
595
+
596
+ return current;
597
+ }
598
+
599
+ /**
600
+ * URL validation and sanitization
601
+ * @private
602
+ */
603
+ _validateAndSanitizeUrl(str) {
604
+ // Check against URL pattern
605
+ if (!ValidationPatterns.url.test(str)) {
606
+ if (this.options.throwOnInvalid) {
607
+ throw new Error('Invalid URL format');
608
+ }
609
+ return '';
610
+ }
611
+
612
+ // Only allow http and https protocols
613
+ if (!str.startsWith('http://') && !str.startsWith('https://')) {
614
+ if (this.options.throwOnInvalid) {
615
+ throw new Error('URL must use http or https protocol');
616
+ }
617
+ return '';
618
+ }
619
+
620
+ return str;
621
+ }
622
+
623
+ /**
624
+ * Email validation
625
+ * @private
626
+ */
627
+ _validateEmail(str) {
628
+ if (!ValidationPatterns.email.test(str)) {
629
+ if (this.options.throwOnInvalid) {
630
+ throw new Error('Invalid email format');
631
+ }
632
+ return '';
633
+ }
634
+ return str.toLowerCase();
635
+ }
636
+
637
+ /**
638
+ * Alphanumeric validation
639
+ * @private
640
+ */
641
+ _validateAlphanumeric(str) {
642
+ if (!ValidationPatterns.alphanumeric.test(str)) {
643
+ if (this.options.throwOnInvalid) {
644
+ throw new Error('Must contain only alphanumeric characters');
645
+ }
646
+ return str.replace(/[^a-zA-Z0-9]/g, '');
647
+ }
648
+ return str;
649
+ }
650
+
651
+ /**
652
+ * UUID validation
653
+ * @private
654
+ */
655
+ _validateUuid(str) {
656
+ if (!ValidationPatterns.uuid.test(str)) {
657
+ if (this.options.throwOnInvalid) {
658
+ throw new Error('Invalid UUID format');
659
+ }
660
+ return '';
661
+ }
662
+ return str.toLowerCase();
663
+ }
664
+
665
+ /**
666
+ * Integer validation
667
+ * @private
668
+ */
669
+ _validateInteger(str) {
670
+ if (!ValidationPatterns.integer.test(str)) {
671
+ if (this.options.throwOnInvalid) {
672
+ throw new Error('Invalid integer format');
673
+ }
674
+ return '';
675
+ }
676
+ return str;
677
+ }
678
+
679
+ /**
680
+ * Float validation
681
+ * @private
682
+ */
683
+ _validateFloat(str) {
684
+ if (!ValidationPatterns.float.test(str)) {
685
+ if (this.options.throwOnInvalid) {
686
+ throw new Error('Invalid float format');
687
+ }
688
+ return '';
689
+ }
690
+ return str;
691
+ }
692
+
693
+ /**
694
+ * Boolean validation
695
+ * @private
696
+ */
697
+ _validateBoolean(str) {
698
+ const lower = str.toLowerCase();
699
+ if (lower === 'true' || lower === '1' || lower === 'yes') {
700
+ return 'true';
701
+ } else if (lower === 'false' || lower === '0' || lower === 'no') {
702
+ return 'false';
703
+ }
704
+
705
+ if (this.options.throwOnInvalid) {
706
+ throw new Error('Invalid boolean value');
707
+ }
708
+ return '';
709
+ }
710
+
711
+ /**
712
+ * JSON validation
713
+ * @private
714
+ */
715
+ _validateJson(str) {
716
+ try {
717
+ JSON.parse(str);
718
+ return str;
719
+ } catch (error) {
720
+ if (this.options.throwOnInvalid) {
721
+ throw new Error('Invalid JSON format');
722
+ }
723
+ return '';
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Base64 validation
729
+ * @private
730
+ */
731
+ _validateBase64(str) {
732
+ if (!ValidationPatterns.base64.test(str)) {
733
+ if (this.options.throwOnInvalid) {
734
+ throw new Error('Invalid base64 format');
735
+ }
736
+ return '';
737
+ }
738
+ return str;
739
+ }
740
+
741
+ /**
742
+ * Hexadecimal validation
743
+ * @private
744
+ */
745
+ _validateHex(str) {
746
+ if (!ValidationPatterns.hex.test(str)) {
747
+ if (this.options.throwOnInvalid) {
748
+ throw new Error('Invalid hexadecimal format');
749
+ }
750
+ return '';
751
+ }
752
+ return str.toLowerCase();
753
+ }
754
+
755
+ /**
756
+ * Slug validation
757
+ * @private
758
+ */
759
+ _validateSlug(str) {
760
+ if (!ValidationPatterns.slug.test(str)) {
761
+ if (this.options.throwOnInvalid) {
762
+ throw new Error('Invalid slug format');
763
+ }
764
+ // Convert to slug
765
+ return str
766
+ .toLowerCase()
767
+ .replace(/[^a-z0-9]+/g, '-')
768
+ .replace(/^-+|-+$/g, '');
769
+ }
770
+ return str;
771
+ }
772
+
773
+ /**
774
+ * Domain validation
775
+ * @private
776
+ */
777
+ _validateDomain(str) {
778
+ if (!ValidationPatterns.domain.test(str)) {
779
+ if (this.options.throwOnInvalid) {
780
+ throw new Error('Invalid domain format');
781
+ }
782
+ return '';
783
+ }
784
+ return str.toLowerCase();
785
+ }
786
+
787
+ /**
788
+ * IPv4 validation
789
+ * @private
790
+ */
791
+ _validateIpv4(str) {
792
+ if (!ValidationPatterns.ipv4.test(str)) {
793
+ if (this.options.throwOnInvalid) {
794
+ throw new Error('Invalid IPv4 address');
795
+ }
796
+ return '';
797
+ }
798
+ return str;
799
+ }
800
+
801
+ /**
802
+ * IPv6 validation
803
+ * @private
804
+ */
805
+ _validateIpv6(str) {
806
+ if (!ValidationPatterns.ipv6.test(str)) {
807
+ if (this.options.throwOnInvalid) {
808
+ throw new Error('Invalid IPv6 address');
809
+ }
810
+ return '';
811
+ }
812
+ return str.toLowerCase();
813
+ }
814
+
815
+ /**
816
+ * Add entry to cache
817
+ * @private
818
+ */
819
+ _addToCache(key, value) {
820
+ // Enforce cache size limit
821
+ if (this.cache.size >= this.options.cacheSize) {
822
+ // Remove oldest entry
823
+ const oldestKey = Array.from(this.cacheAccessTimes.entries()).sort(
824
+ ([, a], [, b]) => a - b,
825
+ )[0][0];
826
+ this.cache.delete(oldestKey);
827
+ this.cacheAccessTimes.delete(oldestKey);
828
+ }
829
+
830
+ this.cache.set(key, value);
831
+ this.cacheAccessTimes.set(key, Date.now());
832
+ }
833
+
834
+ /**
835
+ * Start cache cleanup interval
836
+ * @private
837
+ */
838
+ _startCacheCleanup() {
839
+ this.cacheCleanupInterval = setInterval(() => {
840
+ const now = Date.now();
841
+ const expired = [];
842
+
843
+ for (const [key, accessTime] of this.cacheAccessTimes.entries()) {
844
+ if (now - accessTime > this.options.cacheTTL) {
845
+ expired.push(key);
846
+ }
847
+ }
848
+
849
+ for (const key of expired) {
850
+ this.cache.delete(key);
851
+ this.cacheAccessTimes.delete(key);
852
+ }
853
+ }, this.options.cacheTTL);
854
+
855
+ // Don't block process exit
856
+ if (this.cacheCleanupInterval.unref) {
857
+ this.cacheCleanupInterval.unref();
858
+ }
859
+ }
860
+
861
+ /**
862
+ * Get sanitizer statistics
863
+ *
864
+ * @returns {Object} Statistics object
865
+ */
866
+ getStats() {
867
+ return {
868
+ ...this.stats,
869
+ cacheSize: this.cache.size,
870
+ cacheHitRate: this.stats.cached / Math.max(1, this.stats.sanitized),
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Reset statistics
876
+ */
877
+ resetStats() {
878
+ this.stats = {
879
+ sanitized: 0,
880
+ cached: 0,
881
+ blocked: 0,
882
+ validationErrors: 0,
883
+ averageTime: 0,
884
+ totalTime: 0,
885
+ };
886
+ }
887
+
888
+ /**
889
+ * Clear cache
890
+ */
891
+ clearCache() {
892
+ this.cache.clear();
893
+ this.cacheAccessTimes.clear();
894
+ }
895
+
896
+ /**
897
+ * Destroy sanitizer and cleanup resources
898
+ */
899
+ destroy() {
900
+ if (this.cacheCleanupInterval) {
901
+ clearInterval(this.cacheCleanupInterval);
902
+ this.cacheCleanupInterval = null;
903
+ }
904
+
905
+ this.clearCache();
906
+ }
907
+ }
908
+
909
+ /**
910
+ * Singleton instance with default configuration
911
+ */
912
+ const defaultSanitizer = new InputSanitizer();
913
+
914
+ /**
915
+ * Quick sanitization functions (using singleton)
916
+ */
917
+ function sanitizeInput(input, rules = null) {
918
+ return defaultSanitizer.sanitize(input, rules);
919
+ }
920
+
921
+ function sanitizeHtml(str) {
922
+ return defaultSanitizer.sanitize(str, SanitizationRules.HTML);
923
+ }
924
+
925
+ function sanitizeXss(str) {
926
+ return defaultSanitizer.sanitize(str, SanitizationRules.XSS);
927
+ }
928
+
929
+ function sanitizeSql(str) {
930
+ return defaultSanitizer.sanitize(str, SanitizationRules.SQL);
931
+ }
932
+
933
+ function sanitizeCommand(str) {
934
+ return defaultSanitizer.sanitize(str, SanitizationRules.COMMAND);
935
+ }
936
+
937
+ function sanitizePath(str) {
938
+ return defaultSanitizer.sanitize(str, SanitizationRules.PATH);
939
+ }
940
+
941
+ function validateEmail(str) {
942
+ return defaultSanitizer.sanitize(str, SanitizationRules.EMAIL);
943
+ }
944
+
945
+ function validateUrl(str) {
946
+ return defaultSanitizer.sanitize(str, SanitizationRules.URL);
947
+ }
948
+
949
+ function validateUuid(str) {
950
+ return defaultSanitizer.sanitize(str, SanitizationRules.UUID);
951
+ }
952
+
953
+ module.exports = {
954
+ InputSanitizer,
955
+ SanitizationRules,
956
+ ValidationPatterns,
957
+ sanitizeInput,
958
+ sanitizeHtml,
959
+ sanitizeXss,
960
+ sanitizeSql,
961
+ sanitizeCommand,
962
+ sanitizePath,
963
+ validateEmail,
964
+ validateUrl,
965
+ validateUuid,
966
+ defaultSanitizer,
967
+ };