UNPKG

xss-defender

Version:

A standalone library for XSS sanitization and detection.

633 lines (603 loc) 76 kB
"use strict"; (self["webpackChunkngx_xss_defender"] = self["webpackChunkngx_xss_defender"] || []).push([["commons"],{ /***/ "./spec/core/xss-defender.spec.ts": /*!****************************************!*\ !*** ./spec/core/xss-defender.spec.ts ***! \****************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony import */ var _src_index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../src/index */ "./src/index.ts"); // Предполагая, что index.ts экспортирует все необходимое describe("XssDefender", () => { let defender; beforeEach(() => { // Инициализируем XssDefender с конфигурацией по умолчанию перед каждым тестом // Отключаем логирование для чистоты вывода тестов defender = new _src_index__WEBPACK_IMPORTED_MODULE_0__.XssDefender({ ..._src_index__WEBPACK_IMPORTED_MODULE_0__.DEFAULT_SANITIZATION_CONFIG, enableLogging: false, }); }); describe("sanitizeString", () => { it("should return an empty string for null or undefined input", () => { expect(defender.sanitizeString(null)).toBe(""); expect(defender.sanitizeString(undefined)).toBe(""); }); it("should return an empty string for an empty string input", () => { expect(defender.sanitizeString("")).toBe(""); }); it("should return the same string if it is safe", () => { const safeString = "This is a safe string with 漢字 and 123."; expect(defender.sanitizeString(safeString)).toBe(safeString); }); it("should remove <script> tags by default", () => { const maliciousString = 'Hello <script>alert("XSS")</script> world!'; const expectedString = "Hello world!"; // Пробелы могут остаться в зависимости от реализации replace expect(defender.sanitizeString(maliciousString)).toBe(expectedString); }); it("should remove <script> tags with attributes", () => { const maliciousString = 'Hello <script type="text/javascript">alert("XSS")</script> world!'; const expectedString = "Hello world!"; expect(defender.sanitizeString(maliciousString)).toBe(expectedString); }); it("should remove on<event> handlers", () => { const maliciousString = "<div onclick=\"alert('XSS')\">Click me</div>"; // По умолчанию, div является разрешенным тегом, но onclick должен быть удален // DANGEROUS_PATTERNS удалит 'onclick="alert(\'XSS\')"' целиком const expectedString = "<div>Click me</div>"; // Атрибут onclick удален expect(defender.sanitizeString(maliciousString)).toBe(expectedString); }); it("should encode disallowed tags if stripIgnoreTag is false", () => { defender.setConfig({ allowedTags: ["p"], stripIgnoreTag: false, enableLogging: false, }); const htmlString = "<p>Allowed</p><badtag>Disallowed</badtag><img>"; // <script> и другие DANGEROUS_PATTERNS удаляются до обработки тегов const maliciousScript = '<script>alert("no")</script>'; // будет удалено const mixedString = `<p>Allowed</p><badtag>Disallowed</badtag><img> ${maliciousScript}`; const expectedString = "<p>Allowed</p>&lt;badtag&gt;Disallowed&lt;/badtag&gt;&lt;img&gt; "; expect(defender.sanitizeString(mixedString)).toBe(expectedString); }); }); describe("hasXssRisks", () => { it("should return false for safe strings", () => { expect(defender.hasXssRisks("this is fine")).toBeFalse(); expect(defender.hasXssRisks("")).toBeFalse(); expect(defender.hasXssRisks(null)).toBeFalse(); expect(defender.hasXssRisks(undefined)).toBeFalse(); }); it("should return true for strings with <script> tags", () => { expect(defender.hasXssRisks("<script>alert('XSS')</script>")).toBeTrue(); }); it("should return true for strings with on<event> handlers", () => { expect(defender.hasXssRisks('<img src="x" onerror="alert(\'XSS\')">')).toBeTrue(); }); it("should return true for strings with javascript: protocol", () => { expect(defender.hasXssRisks('href="javascript:alert(1)"')).toBeTrue(); }); it("should return true for strings with data: protocol", () => { expect(defender.hasXssRisks('src="data:text/html;base64,PHNjcmlwdD4="')).toBeTrue(); }); it("should return true for strings with expression() in style", () => { expect(defender.hasXssRisks("<div style=\"width: expression(alert('XSS'))\">")).toBeTrue(); }); }); describe("Llama 3.2 tests XssDefender", () => { it("удаляет классический скрипт", () => { const payload = "<script>alert(1)</script>"; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("обфусцированный регистром JS", () => { const payload = "<JaVaScRiPt>alert(2)</JAvAsCrIpT>"; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("скрытая вставка с комментариями", () => { const payload = "<scr<!-- -->ipt>alert(3)</scr" + "ipt>"; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("использует Unicode-экранирование", () => { const payload = "<\u0073\u0063\u0072\u0069\u0070\u0074>alert(4)</script>"; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("HTML-комментарий внутри тега", () => { const payload = "<scr<!-- -->ipt>alert(5)</script>"; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: простой <script> тег", () => { const payload = '<script>alert("XSS")</script>'; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: <ScRiPt> смешанный регистр", () => { const payload = "текст <ScRiPt>alert(1)</ScRiPt> еще текст"; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: javascript: URL, комментарий, регистр", () => { const payload = '<a hReF="jAvAsCrIpT:alert(1)">link</a>'; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: style expression() с табуляцией", () => { const payload = '<div style="width: exPreSsIoN(\talert(1))"></div>'; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: svg onload с hex и Unicode", () => { const payload = "<s\u0076g/onload=&#" + "x61;lert(1)>"; // 'alert' expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: iframe srcdoc с внутренним скриптом", () => { const payload = '<IFRAME SRCdoc="<img src=1 oNErRoR=alert(1)>"></IFRAME>'; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: data: URL с HTML+JS, регистр", () => { const payload = 'Href="daTa:text/html;base64,PHNjcmlwdD5hbGVydCgiSGVsbG8iKTwvc2NyaXB0Pg=="'; expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("hasXssRisks: onerror с неразрывным пробелом", () => { const payload = '<img src="x" onerror\u00A0="alert(1)">'; // \u00A0 is NBSP expect(defender.hasXssRisks(payload)).toBeTrue(); }); it("sanitizeString: удаляет <script> смешанный регистр", () => { const payload = "Начало <ScR\u0049pT>alert(1)</ScR\u0049pT> Конец"; // <script> удаляется DANGEROUS_PATTERNS expect(defender.sanitizeString(payload)).toBe("Начало Конец"); }); it("sanitizeString: img onerror, Unicode в событии", () => { const payload = '<img src="x" o\u006Eerror="alert(1)">'; // onerror атрибут удаляется DANGEROUS_PATTERNS или фильтром атрибутов // 'img' и 'src' разрешены по умолчанию. expect(defender.sanitizeString(payload)).toBe('<img src="x">'); }); it("sanitizeString: a href javascript: URL, комментарий", () => { const payload = '<a hrEf="jaVaScRiPt:alert(1)">Test</a>'; // javascript: удаляется из href DANGEROUS_PATTERNS // 'a' и 'href' разрешены. expect(defender.sanitizeString(payload)).toBe('<a href="">Test</a>'); }); it("sanitizeString: style expression() с %20 и регистром", () => { const payload = '<div style="color:%20ExpReSsIoN(alert(1))">Text</div>'; expect(defender.sanitizeString(payload)).toBe('<div style="color:">Text</div>'); }); it("sanitizeString: svg onload, hex в теге", () => { const payload = '<s&#x76;g onload="alert(1)"></s&#x76;g>'; // onload удаляется DANGEROUS_PATTERNS. // <svg> не разрешен -> кодируется. expect(defender.sanitizeString(payload)).toBe("&lt;svg &gt;&lt;/svg&gt;"); }); it("sanitizeString: iframe src с TAB и JS URL", () => { const payload = '<IFRAME\tsrc="javascript:alert(1)"></IFRAME>'; // javascript: удаляется DANGEROUS_PATTERNS. // <iframe> не разрешен -> кодируется. expect(defender.sanitizeString(payload)).toBe('&lt;IFRAME\tsrc=""&gt;&lt;/IFRAME&gt;'); }); it("sanitizeString: data: URL в href, Unicode в протоколе", () => { const payload = '<a hRef="d\u0061Ta:text/html,<script>alert(1)</script>">X</a>'; // data:text/html со <script> удаляется из href DANGEROUS_PATTERNS expect(defender.sanitizeString(payload)).toBe('<a href="">X</a>'); }); it("sanitizeString: текст и <ScRiPt> %20 кодирование", () => { const payload = "SafeText <ScRiPt%20sRc='x.js'></ScRiPt%20> End"; // <script> удаляется DANGEROUS_PATTERNS expect(defender.sanitizeString(payload)).toBe("SafeText End"); }); it("sanitizeString: img OnMousEOVER с HTML-комментарием", () => { const payload = '<img src="ok.png" OnMousEOVER="alert(1)">'; // OnMousEOVER удаляется DANGEROUS_PATTERNS или фильтром атрибутов. expect(defender.sanitizeString(payload)).toBe('<img src="ok.png">'); }); it("sanitizeString: ссылка с URL Unicode обфускацией JS", () => { const payload = '<a hRef="\u006a\u0061\u0076\u0061script:alert(1)">Link</a>'; // javascript: удаляется DANGEROUS_PATTERNS expect(defender.sanitizeString(payload)).toBe('<a href="">Link</a>'); }); }); }); /***/ }), /***/ "./src/core/config.ts": /*!****************************!*\ !*** ./src/core/config.ts ***! \****************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ DEFAULT_SANITIZATION_CONFIG: () => (/* binding */ DEFAULT_SANITIZATION_CONFIG) /* harmony export */ }); /** * Default sanitization configuration. */ const DEFAULT_SANITIZATION_CONFIG = { allowedTags: [ "p", "br", "b", "i", "ul", "ol", "li", "span", "div", "a", "img", ], allowedAttributes: ["id", "class", "style", "href", "target", "src"], stripIgnoreTag: false, enableLogging: false, logFormat: "simple", }; /***/ }), /***/ "./src/core/logger.ts": /*!****************************!*\ !*** ./src/core/logger.ts ***! \****************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Logger: () => (/* binding */ Logger) /* harmony export */ }); /** * Logger class for XssDefender activities. */ class Logger { /** * Logs an informational message. * @param message The message to log. * @param config The current sanitization configuration. */ log(message, config) { if (config.enableLogging) { console.info(`XssDefender: ${message}`); } } /** * Logs a warning message. * @param message The warning message. * @param details Optional details to include in the log (used if logFormat is 'detailed'). * @param config The current sanitization configuration. */ warn(message, details, config) { if (config?.enableLogging) { if (config.logFormat === "detailed" && details) { console.warn(`XssDefender: ${message}`, details); } else { console.warn(`XssDefender: ${message}`); } } } } /***/ }), /***/ "./src/core/patterns.ts": /*!******************************!*\ !*** ./src/core/patterns.ts ***! \******************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ DANGEROUS_PATTERNS: () => (/* binding */ DANGEROUS_PATTERNS) /* harmony export */ }); /** * Regular expressions for detecting common XSS attack vectors. * These patterns are used to identify and remove or neutralize malicious code. */ const DANGEROUS_PATTERNS = [ // 1. CSS Expressions (усиленный для пробелов и комментариев) /(?:\/\*[\s\S]*?\*\/)?(?:\s|%[0-9a-fA-F]{2})*expression[\s\u00A0]*\((?:[^(){};]+|\((?:[^(){};]+|\([^(){};]*\))*\))*\)/gi, // 2. "on..." event handler attributes (NBSP-aware) /[\s\u00A0]*on\w+(?:\/\*.*?\*\/)?[\s\u00A0]*=[\s\u00A0]*(?:"(?:(?!"|\\).)*?"|'(?:(?!'|\\).)*?'|[^\s>]*)/gi, // 3. Script tags - более полный паттерн /<\s*s(?:[\s%0-9a-fA-F]*?)c(?:[\s%0-9a-fA-F]*?)r(?:[\s%0-9a-fA-F]*?)i(?:[\s%0-9a-fA-F]*?)p(?:[\s%0-9a-fA-F]*?)t\b[^>]*>[\s\S]*?<\s*\/\s*s(?:[\s%0-9a-fA-F]*?)c(?:[\s%0-9a-fA-F]*?)r(?:[\s%0-9a-fA-F]*?)i(?:[\s%0-9a-fA-F]*?)p(?:[\s%0-9a-fA-F]*?)t\s*>/gi, // 3-б) Одиночные <script …> или </script> /<\s*\/?\s*s(?:[\s%0-9a-fA-F]*?)c(?:[\s%0-9a-fA-F]*?)r(?:[\s%0-9a-fA-F]*?)i(?:[\s%0-9a-fA-F]*?)p(?:[\s%0-9a-fA-F]*?)t\b[^>]*>/gi, /<\/?javascript[^>]*>[\s\S]*?<\/javascript[^>]*>/gi, /<javascript[^>]*>/gi, /<\/javascript[^>]*>/gi, // 4. JavaScript protocol (обновленный для Unicode обфускации) /(?<=href[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|&colon;)+/gi, /(?<=src[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|&colon;)+/gi, /(?<=action[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|&colon;)+/gi, /(?<=formaction[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*(?:ja(?:\/\*.*?\*\/)?va(?:\/\*.*?\*\/)?script|\\u006a\\u0061\\u0076\\u0061script):(?:[^"'>\s\u00A0]|&colon;)+/gi, // Простые версии для javascript: /javascript[\s\u00A0]*:/gi, /\\u006a\\u0061\\u0076\\u0061script[\s\u00A0]*:/gi, // 5. data: protocol для опасных MIME-типов /(?<=href[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*data:(?:text\/(?:html|xml)|application\/(?:xml|xhtml\+xml|javascript|json|rss\+xml)|image\/svg\+xml)[^"'>\s,]*,[^"'>\s\u00A0]*/gi, /(?<=src[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*data:(?:text\/(?:html|xml)|application\/(?:xml|xhtml\+xml|javascript|json|rss\+xml)|image\/svg\+xml)[^"'>\s,]*,[^"'>\s\u00A0]*/gi, /(?<=srcdoc[\s\u00A0]*=[\s\u00A0]*(?:"|')?)\s*data:(?:text\/(?:html|xml)|application\/(?:xml|xhtml\+xml|javascript|json|rss\+xml)|image\/svg\+xml)[^"'>\s,]*,[^"'>\s\u00A0]*/gi, // Общий data: для hasXssRisks /\bdata:/gi, /\\u0064\\u0061\\u0054\\u0061:/gi, // Unicode обфускация для data: // 6. Дополнительные паттерны для iframe srcdoc /srcdoc[\s\u00A0]*=[\s\u00A0]*(?:"[^"]*(?:script|onerror|onload)[^"]*"|'[^']*(?:script|onerror|onload)[^']*')/gi, // 7. HTML комментарии с script /<!--[\s\S]*?<\s*\/?\s*s\s*?c\s*?r\s*?i\s*?p\s*?t\b[\s\S]*?-->/gi, ]; /***/ }), /***/ "./src/core/xss-defender.ts": /*!**********************************!*\ !*** ./src/core/xss-defender.ts ***! \**********************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ XssDefender: () => (/* binding */ XssDefender) /* harmony export */ }); /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./config */ "./src/core/config.ts"); /* harmony import */ var _patterns__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./patterns */ "./src/core/patterns.ts"); /* harmony import */ var _logger__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./logger */ "./src/core/logger.ts"); /** * Main class for XSS sanitization and defense. * Provides methods to sanitize strings, HTML content, objects, and URL parameters. */ class XssDefender { /** * Initializes a new instance of the XssDefender. * @param initialConfig Optional initial configuration to override defaults. */ constructor(initialConfig) { this.currentConfig = { ..._config__WEBPACK_IMPORTED_MODULE_0__.DEFAULT_SANITIZATION_CONFIG, ...initialConfig }; this.logger = new _logger__WEBPACK_IMPORTED_MODULE_2__.Logger(); if (this.currentConfig.enableLogging) { this.logger.log("XssDefender initialized.", this.currentConfig); } } /** * Updates the current sanitization configuration. * @param config Partial configuration object to merge with the current settings. */ setConfig(config) { this.currentConfig = { ...this.currentConfig, ...config }; if (this.currentConfig.enableLogging) { this.logger.log("Configuration updated.", this.currentConfig); } } /** * Retrieves the current sanitization configuration. * @returns A read-only copy of the current configuration. */ getConfig() { return this.currentConfig; } /** * Performs basic sanitization of a string value based on the provided configuration. * This includes stripping dangerous patterns, and handling allowed/disallowed tags and attributes. * @param value The string to sanitize. * @param config The sanitization configuration to use. * @returns The sanitized string. */ _basicSanitization(value, config) { let sanitized = this.decodeNumericEntities(value); sanitized = sanitized.replace(/(\s|\u00A0)+on\w+(?:\/\*[\s\S]*?\*\/)?[\s\u00A0]*=\s*(?:(["']).*?\2|[^\s>]+)/gi, "$1"); // 1. Remove known dangerous patterns _patterns__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_PATTERNS.forEach((pattern) => { sanitized = sanitized.replace(pattern, ""); }); // 2. Handle HTML tags if (config.allowedTags && config.allowedTags.length > 0) { // Pattern to find tags that are NOT in the allowed list const tagsToRemovePattern = new RegExp(`</?(?!(?:${config.allowedTags.join("|")})(?:\\s|>|/))[a-zA-Z0-9]+[^>]*>`, "gi"); sanitized = sanitized.replace(tagsToRemovePattern, (match) => { if (config.stripIgnoreTag) { return ""; // Remove the disallowed tag } // Encode the disallowed tag return match.replace(/</g, "&lt;").replace(/>/g, "&gt;"); }); } else { // No tags are allowed if (config.stripIgnoreTag) { // Remove all tags sanitized = sanitized.replace(/<[^>]+>/gi, ""); } else { // Encode all tags sanitized = sanitized.replace(/<(\/?[\w\d\s="/.'-]+?)>/gi, (m, tagContent) => `&lt;${tagContent}&gt;`); } } // 3. Handle HTML attributes (only for tags that are allowed or were not stripped) if (config.allowedAttributes && config.allowedAttributes.length > 0) { const universalTagPattern = /<([a-zA-Z0-9]+)((?:\s+[^>]*)?)>/g; sanitized = sanitized.replace(universalTagPattern, (match, tagName, attributesString) => { const lowerTagName = tagName.toLowerCase(); // Skip if the tag itself is not allowed if (config.allowedTags && !config.allowedTags.includes(lowerTagName)) { return match; } const attributePattern = /\s*([a-zA-Z0-9\-_]+)\s*=\s*(?:(["'])(.*?)\2|([^>\s]+))/g; let newAttributesString = ""; let attrMatch; while ((attrMatch = attributePattern.exec(attributesString)) !== null) { const attributeName = attrMatch[1].toLowerCase(); if (config.allowedAttributes?.includes(attributeName)) { let attributeValue = attrMatch[3] !== undefined ? attrMatch[3] : attrMatch[4]; const escapedAttributeValue = attributeValue.replace(/"/g, "&quot;"); newAttributesString += ` ${attributeName}="${escapedAttributeValue}"`; } } return `<${tagName}${newAttributesString}>`; }); } else { // No attributes allowed, remove all attributes from all tags const allAttributesPattern = /\s+[a-zA-Z0-9\-_]+\s*=\s*(?:(["']).*?\1|[^>\s]+)/g; sanitized = sanitized.replace(/<([a-zA-Z0-9]+)((?:\s+[^>]*)?)>/g, (match, tagName, attributesString) => { return `<${tagName}${attributesString.replace(allAttributesPattern, "")}>`; }); } return sanitized; } /** * Sanitizes a string, removing potential XSS threats. * @param value The string to sanitize. Can be null or undefined, in which case an empty string is returned. * @returns The sanitized string. */ sanitizeString(value) { if (value === null || value === undefined || value === "") return ""; const originalValue = String(value); let sanitizedValue = originalValue; sanitizedValue = this._basicSanitization(sanitizedValue, this.currentConfig); if (originalValue !== sanitizedValue) { this._logDetection(originalValue, sanitizedValue); } return sanitizedValue; } /** * Sanitizes an HTML string and sets it as the innerHTML of a given HTMLElement. * @param element The HTMLElement whose innerHTML is to be set. * @param unsafeHtml The potentially unsafe HTML string to sanitize and apply. */ sanitizeHtmlForElement(element, unsafeHtml) { if (!element) return; const sanitizedHtml = this.sanitizeString(unsafeHtml); element.innerHTML = sanitizedHtml; } /** * Recursively sanitizes all string values within an object or an array. * @param obj The object or array to sanitize. * @returns The sanitized object or array. */ sanitizeObject(obj) { if (obj === null || typeof obj !== "object") { if (typeof obj === "string") { return this.sanitizeString(obj); } return obj; } if (Array.isArray(obj)) { return obj.map((item) => this.sanitizeObject(item)); } const result = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (typeof value === "string") { result[key] = this.sanitizeString(value); } else if (typeof value === "object") { result[key] = this.sanitizeObject(value); } else { result[key] = value; } } } return result; } /** * Checks URL parameters for potential XSS risks and returns sanitized versions. * @param params A record of URL parameters. * @returns An object indicating if all parameters are safe and a list of any issues found. */ checkUrlParams(params) { const issues = []; let isSafe = true; for (const key in params) { if (Object.prototype.hasOwnProperty.call(params, key)) { const paramValue = params[key]; const valuesToCheck = Array.isArray(paramValue) ? paramValue : paramValue ? [paramValue] : []; for (const originalSingleValue of valuesToCheck) { if (typeof originalSingleValue !== "string") continue; const sanitizedSingleValue = this.sanitizeString(originalSingleValue); // An issue is logged if sanitization changed the value OR if the original value had detectable XSS patterns // (even if sanitizeString somehow missed it or the patterns are different) if (originalSingleValue !== sanitizedSingleValue || this.hasXssRisks(originalSingleValue)) { if (originalSingleValue !== sanitizedSingleValue) { // Prefer logging if actual change occurred isSafe = false; issues.push({ key, value: sanitizedSingleValue, originalValue: originalSingleValue, }); this.logger.warn(`Potential XSS risk in URL parameter "${key}" was sanitized.`, { key, originalValue: originalSingleValue, sanitizedValue: sanitizedSingleValue, timestamp: new Date().toISOString(), }, this.currentConfig); } else if (this.hasXssRisks(originalSingleValue)) { // Log if original had risk, even if sanitization resulted in same string (less likely) isSafe = false; // Still mark as not entirely safe due to initial risk issues.push({ key, value: sanitizedSingleValue, originalValue: originalSingleValue, }); // Report original and "sanitized" this.logger.warn(`Potential XSS risk detected in URL parameter "${key}". Input was already clean or sanitization was ineffective.`, { key, originalValue: originalSingleValue, sanitizedValue: sanitizedSingleValue, timestamp: new Date().toISOString(), }, this.currentConfig); } } } } } return { isSafe, issues }; } /** * Checks if a given string contains known XSS risk patterns. * This method tests the string against `DANGEROUS_PATTERNS`. * @param value The string to check. Can be null or undefined. * @returns `true` if XSS risks are found, `false` otherwise. */ hasXssRisks(value) { if (!value) return false; const str = String(value); return _patterns__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_PATTERNS.some((p) => new RegExp(p.source, p.flags).test(str)); } decodeNumericEntities(str) { return (str // &#xNNNN; .replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))) // &#NNNN; .replace(/&#(\d+);?/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)))); } /** * Logs a detection event when sanitization modifies the input string. * @param originalValue The original, unsafe string. * @param sanitizedValue The sanitized string. */ _logDetection(originalValue, sanitizedValue) { const details = { originalValue, sanitizedValue, configUsed: { // Log only key aspects of config for brevity unless detailed allowedTags: this.currentConfig.allowedTags, allowedAttributes: this.currentConfig.allowedAttributes, stripIgnoreTag: this.currentConfig.stripIgnoreTag, }, timestamp: new Date().toISOString(), }; this.logger.warn("Potential XSS detected and input sanitized.", details, this.currentConfig); } } /***/ }), /***/ "./src/index.ts": /*!**********************!*\ !*** ./src/index.ts ***! \**********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ DEFAULT_SANITIZATION_CONFIG: () => (/* reexport safe */ _core_config__WEBPACK_IMPORTED_MODULE_1__.DEFAULT_SANITIZATION_CONFIG), /* harmony export */ XssDefender: () => (/* reexport safe */ _core_xss_defender__WEBPACK_IMPORTED_MODULE_0__.XssDefender) /* harmony export */ }); /* harmony import */ var _core_xss_defender__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./core/xss-defender */ "./src/core/xss-defender.ts"); /* harmony import */ var _core_config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./core/config */ "./src/core/config.ts"); /***/ }) }]); //# sourceMappingURL=data:application/json;charset=utf-8;base64,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QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxhQUFhO0FBQ2I7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FDdlFrRDtBQUNVIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vbmd4LXhzcy1kZWZlbmRlci8uL3NwZWMvY29yZS94c3MtZGVmZW5kZXIuc3BlYy50cyIsIndlYnBhY2s6Ly9uZ3gteHNzLWRlZmVuZGVyLy4vc3JjL2NvcmUvY29uZmlnLnRzIiwid2VicGFjazovL25neC14c3MtZGVmZW5kZXIvLi9zcmMvY29yZS9sb2dnZXIudHMiLCJ3ZWJwYWNrOi8vbmd4LXhzcy1kZWZlbmRlci8uL3NyYy9jb3JlL3BhdHRlcm5zLnRzIiwid2VicGFjazovL25neC14c3MtZGVmZW5kZXIvLi9zcmMvY29yZS94c3MtZGVmZW5kZXIudHMiLCJ3ZWJwYWNrOi8vbmd4LXhzcy1kZWZlbmRlci8uL3NyYy9pbmRleC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBYc3NEZWZlbmRlciwgREVGQVVMVF9TQU5JVElaQVRJT05fQ09ORklHIH0gZnJvbSBcIi4uLy4uL3NyYy9pbmRleFwiOyAvLyDQn9GA0LXQtNC/0L7Qu9Cw0LPQsNGPLCDRh9GC0L4gaW5kZXgudHMg0Y3QutGB0L/QvtGA0YLQuNGA0YPQtdGCINCy0YHQtSDQvdC10L7QsdGF0L7QtNC40LzQvtC1XG5kZXNjcmliZShcIlhzc0RlZmVuZGVyXCIsICgpID0+IHtcbiAgICBsZXQgZGVmZW5kZXI7XG4gICAgYmVmb3JlRWFjaCgoKSA9PiB7XG4gICAgICAgIC8vINCY0L3QuNGG0LjQsNC70LjQt9C40YDRg9C10LwgWHNzRGVmZW5kZXIg0YEg0LrQvtC90YTQuNCz0YPRgNCw0YbQuNC10Lkg0L/QviDRg9C80L7Qu9GH0LDQvdC40Y4g0L/QtdGA0LXQtCDQutCw0LbQtNGL0Lwg0YLQtdGB0YLQvtC8XG4gICAgICAgIC8vINCe0YLQutC70Y7Rh9Cw0LXQvCDQu9C+0LPQuNGA0L7QstCw0L3QuNC1INC00LvRjyDRh9C40YHRgtC+0YLRiyDQstGL0LLQvtC00LAg0YLQtdGB0YLQvtCyXG4gICAgICAgIGRlZmVuZGVyID0gbmV3IFhzc0RlZmVuZGVyKHtcbiAgICAgICAgICAgIC4uLkRFRkFVTFRfU0FOSVRJWkFUSU9OX0NPTkZJRyxcbiAgICAgICAgICAgIGVuYWJsZUxvZ2dpbmc6IGZhbHNlLFxuICAgICAgICB9KTtcbiAgICB9KTtcbiAgICBkZXNjcmliZShcInNhbml0aXplU3RyaW5nXCIsICgpID0+IHtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIGFuIGVtcHR5IHN0cmluZyBmb3IgbnVsbCBvciB1bmRlZmluZWQgaW5wdXRcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG51bGwpKS50b0JlKFwiXCIpO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHVuZGVmaW5lZCkpLnRvQmUoXCJcIik7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gYW4gZW1wdHkgc3RyaW5nIGZvciBhbiBlbXB0eSBzdHJpbmcgaW5wdXRcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKFwiXCIpKS50b0JlKFwiXCIpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIHRoZSBzYW1lIHN0cmluZyBpZiBpdCBpcyBzYWZlXCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHNhZmVTdHJpbmcgPSBcIlRoaXMgaXMgYSBzYWZlIHN0cmluZyB3aXRoIOa8ouWtlyBhbmQgMTIzLlwiO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHNhZmVTdHJpbmcpKS50b0JlKHNhZmVTdHJpbmcpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmVtb3ZlIDxzY3JpcHQ+IHRhZ3MgYnkgZGVmYXVsdFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBtYWxpY2lvdXNTdHJpbmcgPSAnSGVsbG8gPHNjcmlwdD5hbGVydChcIlhTU1wiKTwvc2NyaXB0PiB3b3JsZCEnO1xuICAgICAgICAgICAgY29uc3QgZXhwZWN0ZWRTdHJpbmcgPSBcIkhlbGxvICB3b3JsZCFcIjsgLy8g0J/RgNC+0LHQtdC70Ysg0LzQvtCz0YPRgiDQvtGB0YLQsNGC0YzRgdGPINCyINC30LDQstC40YHQuNC80L7RgdGC0Lgg0L7RgiDRgNC10LDQu9C40LfQsNGG0LjQuCByZXBsYWNlXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcobWFsaWNpb3VzU3RyaW5nKSkudG9CZShleHBlY3RlZFN0cmluZyk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZW1vdmUgPHNjcmlwdD4gdGFncyB3aXRoIGF0dHJpYnV0ZXNcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgbWFsaWNpb3VzU3RyaW5nID0gJ0hlbGxvIDxzY3JpcHQgdHlwZT1cInRleHQvamF2YXNjcmlwdFwiPmFsZXJ0KFwiWFNTXCIpPC9zY3JpcHQ+IHdvcmxkISc7XG4gICAgICAgICAgICBjb25zdCBleHBlY3RlZFN0cmluZyA9IFwiSGVsbG8gIHdvcmxkIVwiO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG1hbGljaW91c1N0cmluZykpLnRvQmUoZXhwZWN0ZWRTdHJpbmcpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmVtb3ZlIG9uPGV2ZW50PiBoYW5kbGVyc1wiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBtYWxpY2lvdXNTdHJpbmcgPSBcIjxkaXYgb25jbGljaz1cXFwiYWxlcnQoJ1hTUycpXFxcIj5DbGljayBtZTwvZGl2PlwiO1xuICAgICAgICAgICAgLy8g0J/QviDRg9C80L7Qu9GH0LDQvdC40Y4sIGRpdiDRj9Cy0LvRj9C10YLRgdGPINGA0LDQt9GA0LXRiNC10L3QvdGL0Lwg0YLQtdCz0L7QvCwg0L3QviBvbmNsaWNrINC00L7Qu9C20LXQvSDQsdGL0YLRjCDRg9C00LDQu9C10L1cbiAgICAgICAgICAgIC8vIERBTkdFUk9VU19QQVRURVJOUyDRg9C00LDQu9C40YIgJ29uY2xpY2s9XCJhbGVydChcXCdYU1NcXCcpXCInINGG0LXQu9C40LrQvtC8XG4gICAgICAgICAgICBjb25zdCBleHBlY3RlZFN0cmluZyA9IFwiPGRpdj5DbGljayBtZTwvZGl2PlwiOyAvLyDQkNGC0YDQuNCx0YPRgiBvbmNsaWNrINGD0LTQsNC70LXQvVxuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG1hbGljaW91c1N0cmluZykpLnRvQmUoZXhwZWN0ZWRTdHJpbmcpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgZW5jb2RlIGRpc2FsbG93ZWQgdGFncyBpZiBzdHJpcElnbm9yZVRhZyBpcyBmYWxzZVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBkZWZlbmRlci5zZXRDb25maWcoe1xuICAgICAgICAgICAgICAgIGFsbG93ZWRUYWdzOiBbXCJwXCJdLFxuICAgICAgICAgICAgICAgIHN0cmlwSWdub3JlVGFnOiBmYWxzZSxcbiAgICAgICAgICAgICAgICBlbmFibGVMb2dnaW5nOiBmYWxzZSxcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgY29uc3QgaHRtbFN0cmluZyA9IFwiPHA+QWxsb3dlZDwvcD48YmFkdGFnPkRpc2FsbG93ZWQ8L2JhZHRhZz48aW1nPlwiO1xuICAgICAgICAgICAgLy8gPHNjcmlwdD4g0Lgg0LTRgNGD0LPQuNC1IERBTkdFUk9VU19QQVRURVJOUyDRg9C00LDQu9GP0Y7RgtGB0Y8g0LTQviDQvtCx0YDQsNCx0L7RgtC60Lgg0YLQtdCz0L7QslxuICAgICAgICAgICAgY29uc3QgbWFsaWNpb3VzU2NyaXB0ID0gJzxzY3JpcHQ+YWxlcnQoXCJub1wiKTwvc2NyaXB0Pic7IC8vINCx0YPQtNC10YIg0YPQtNCw0LvQtdC90L5cbiAgICAgICAgICAgIGNvbnN0IG1peGVkU3RyaW5nID0gYDxwPkFsbG93ZWQ8L3A+PGJhZHRhZz5EaXNhbGxvd2VkPC9iYWR0YWc+PGltZz4gJHttYWxpY2lvdXNTY3JpcHR9YDtcbiAgICAgICAgICAgIGNvbnN0IGV4cGVjdGVkU3RyaW5nID0gXCI8cD5BbGxvd2VkPC9wPiZsdDtiYWR0YWcmZ3Q7RGlzYWxsb3dlZCZsdDsvYmFkdGFnJmd0OyZsdDtpbWcmZ3Q7IFwiO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKG1peGVkU3RyaW5nKSkudG9CZShleHBlY3RlZFN0cmluZyk7XG4gICAgICAgIH0pO1xuICAgIH0pO1xuICAgIGRlc2NyaWJlKFwiaGFzWHNzUmlza3NcIiwgKCkgPT4ge1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gZmFsc2UgZm9yIHNhZmUgc3RyaW5nc1wiLCAoKSA9PiB7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MoXCJ0aGlzIGlzIGZpbmVcIikpLnRvQmVGYWxzZSgpO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKFwiXCIpKS50b0JlRmFsc2UoKTtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhudWxsKSkudG9CZUZhbHNlKCk7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3ModW5kZWZpbmVkKSkudG9CZUZhbHNlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gdHJ1ZSBmb3Igc3RyaW5ncyB3aXRoIDxzY3JpcHQ+IHRhZ3NcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKFwiPHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD5cIikpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gdHJ1ZSBmb3Igc3RyaW5ncyB3aXRoIG9uPGV2ZW50PiBoYW5kbGVyc1wiLCAoKSA9PiB7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MoJzxpbWcgc3JjPVwieFwiIG9uZXJyb3I9XCJhbGVydChcXCdYU1NcXCcpXCI+JykpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNob3VsZCByZXR1cm4gdHJ1ZSBmb3Igc3RyaW5ncyB3aXRoIGphdmFzY3JpcHQ6IHByb3RvY29sXCIsICgpID0+IHtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcygnaHJlZj1cImphdmFzY3JpcHQ6YWxlcnQoMSlcIicpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIHRydWUgZm9yIHN0cmluZ3Mgd2l0aCBkYXRhOiBwcm90b2NvbFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MoJ3NyYz1cImRhdGE6dGV4dC9odG1sO2Jhc2U2NCxQSE5qY21sd2REND1cIicpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzaG91bGQgcmV0dXJuIHRydWUgZm9yIHN0cmluZ3Mgd2l0aCBleHByZXNzaW9uKCkgaW4gc3R5bGVcIiwgKCkgPT4ge1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKFwiPGRpdiBzdHlsZT1cXFwid2lkdGg6IGV4cHJlc3Npb24oYWxlcnQoJ1hTUycpKVxcXCI+XCIpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICB9KTtcbiAgICBkZXNjcmliZShcIkxsYW1hIDMuMiB0ZXN0cyBYc3NEZWZlbmRlclwiLCAoKSA9PiB7XG4gICAgICAgIGl0KFwi0YPQtNCw0LvRj9C10YIg0LrQu9Cw0YHRgdC40YfQtdGB0LrQuNC5INGB0LrRgNC40L/RglwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCI8c2NyaXB0PmFsZXJ0KDEpPC9zY3JpcHQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcItC+0LHRhNGD0YHRhtC40YDQvtCy0LDQvdC90YvQuSDRgNC10LPQuNGB0YLRgNC+0LwgSlNcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9IFwiPEphVmFTY1JpUHQ+YWxlcnQoMik8L0pBdkFzQ3JJcFQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcItGB0LrRgNGL0YLQsNGPINCy0YHRgtCw0LLQutCwINGBINC60L7QvNC80LXQvdGC0LDRgNC40Y/QvNC4XCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSBcIjxzY3I8IS0tIC0tPmlwdD5hbGVydCgzKTwvc2NyXCIgKyBcImlwdD5cIjtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwi0LjRgdC/0L7Qu9GM0LfRg9C10YIgVW5pY29kZS3RjdC60YDQsNC90LjRgNC+0LLQsNC90LjQtVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCI8XFx1MDA3M1xcdTAwNjNcXHUwMDcyXFx1MDA2OVxcdTAwNzBcXHUwMDc0PmFsZXJ0KDQpPC9zY3JpcHQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcIkhUTUwt0LrQvtC80LzQtdC90YLQsNGA0LjQuSDQstC90YPRgtGA0Lgg0YLQtdCz0LBcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9IFwiPHNjcjwhLS0gLS0+aXB0PmFsZXJ0KDUpPC9zY3JpcHQ+XCI7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcImhhc1hzc1Jpc2tzOiDQv9GA0L7RgdGC0L7QuSA8c2NyaXB0PiDRgtC10LNcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9ICc8c2NyaXB0PmFsZXJ0KFwiWFNTXCIpPC9zY3JpcHQ+JztcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IDxTY1JpUHQ+INGB0LzQtdGI0LDQvdC90YvQuSDRgNC10LPQuNGB0YLRgFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCLRgtC10LrRgdGCIDxTY1JpUHQ+YWxlcnQoMSk8L1NjUmlQdD4g0LXRidC1INGC0LXQutGB0YJcIjtcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IGphdmFzY3JpcHQ6IFVSTCwg0LrQvtC80LzQtdC90YLQsNGA0LjQuSwg0YDQtdCz0LjRgdGC0YBcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9ICc8YSBoUmVGPVwiakF2QXNDcklwVDphbGVydCgxKVwiPmxpbms8L2E+JztcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IHN0eWxlIGV4cHJlc3Npb24oKSDRgSDRgtCw0LHRg9C70Y/RhtC40LXQuVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxkaXYgc3R5bGU9XCJ3aWR0aDogZXhQcmVTc0lvTihcXHRhbGVydCgxKSlcIj48L2Rpdj4nO1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKHBheWxvYWQpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJoYXNYc3NSaXNrczogc3ZnIG9ubG9hZCDRgSBoZXgg0LggVW5pY29kZVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCI8c1xcdTAwNzZnL29ubG9hZD0mI1wiICsgXCJ4NjE7bGVydCgxKT5cIjsgLy8gJ2FsZXJ0J1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLmhhc1hzc1Jpc2tzKHBheWxvYWQpKS50b0JlVHJ1ZSgpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJoYXNYc3NSaXNrczogaWZyYW1lIHNyY2RvYyDRgSDQstC90YPRgtGA0LXQvdC90LjQvCDRgdC60YDQuNC/0YLQvtC8XCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSAnPElGUkFNRSBTUkNkb2M9XCI8aW1nIHNyYz0xIG9ORXJSb1I9YWxlcnQoMSk+XCI+PC9JRlJBTUU+JztcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwiaGFzWHNzUmlza3M6IGRhdGE6IFVSTCDRgSBIVE1MK0pTLCDRgNC10LPQuNGB0YLRgFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJ0hyZWY9XCJkYVRhOnRleHQvaHRtbDtiYXNlNjQsUEhOamNtbHdkRDVoYkdWeWRDZ2lTR1ZzYkc4aUtUd3ZjMk55YVhCMFBnPT1cIic7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuaGFzWHNzUmlza3MocGF5bG9hZCkpLnRvQmVUcnVlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcImhhc1hzc1Jpc2tzOiBvbmVycm9yINGBINC90LXRgNCw0LfRgNGL0LLQvdGL0Lwg0L/RgNC+0LHQtdC70L7QvFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxpbWcgc3JjPVwieFwiIG9uZXJyb3JcXHUwMEEwPVwiYWxlcnQoMSlcIj4nOyAvLyBcXHUwMEEwIGlzIE5CU1BcbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5oYXNYc3NSaXNrcyhwYXlsb2FkKSkudG9CZVRydWUoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6INGD0LTQsNC70Y/QtdGCIDxzY3JpcHQ+INGB0LzQtdGI0LDQvdC90YvQuSDRgNC10LPQuNGB0YLRgFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gXCLQndCw0YfQsNC70L4gPFNjUlxcdTAwNDlwVD5hbGVydCgxKTwvU2NSXFx1MDA0OXBUPiDQmtC+0L3QtdGGXCI7XG4gICAgICAgICAgICAvLyA8c2NyaXB0PiDRg9C00LDQu9GP0LXRgtGB0Y8gREFOR0VST1VTX1BBVFRFUk5TXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoXCLQndCw0YfQsNC70L4gINCa0L7QvdC10YZcIik7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNhbml0aXplU3RyaW5nOiBpbWcgb25lcnJvciwgVW5pY29kZSDQsiDRgdC+0LHRi9GC0LjQuFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxpbWcgc3JjPVwieFwiIG9cXHUwMDZFZXJyb3I9XCJhbGVydCgxKVwiPic7XG4gICAgICAgICAgICAvLyBvbmVycm9yINCw0YLRgNC40LHRg9GCINGD0LTQsNC70Y/QtdGC0YHRjyBEQU5HRVJPVVNfUEFUVEVSTlMg0LjQu9C4INGE0LjQu9GM0YLRgNC+0Lwg0LDRgtGA0LjQsdGD0YLQvtCyXG4gICAgICAgICAgICAvLyAnaW1nJyDQuCAnc3JjJyDRgNCw0LfRgNC10YjQtdC90Ysg0L/QviDRg9C80L7Qu9GH0LDQvdC40Y4uXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoJzxpbWcgc3JjPVwieFwiPicpO1xuICAgICAgICB9KTtcbiAgICAgICAgaXQoXCJzYW5pdGl6ZVN0cmluZzogYSBocmVmIGphdmFzY3JpcHQ6IFVSTCwg0LrQvtC80LzQtdC90YLQsNGA0LjQuVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxhIGhyRWY9XCJqYVZhU2NSaVB0OmFsZXJ0KDEpXCI+VGVzdDwvYT4nO1xuICAgICAgICAgICAgLy8gamF2YXNjcmlwdDog0YPQtNCw0LvRj9C10YLRgdGPINC40LcgaHJlZiBEQU5HRVJPVVNfUEFUVEVSTlNcbiAgICAgICAgICAgIC8vICdhJyDQuCAnaHJlZicg0YDQsNC30YDQtdGI0LXQvdGLLlxuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHBheWxvYWQpKS50b0JlKCc8YSBocmVmPVwiXCI+VGVzdDwvYT4nKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6IHN0eWxlIGV4cHJlc3Npb24oKSDRgSAlMjAg0Lgg0YDQtdCz0LjRgdGC0YDQvtC8XCIsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHBheWxvYWQgPSAnPGRpdiBzdHlsZT1cImNvbG9yOiUyMEV4cFJlU3NJb04oYWxlcnQoMSkpXCI+VGV4dDwvZGl2Pic7XG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoJzxkaXYgc3R5bGU9XCJjb2xvcjpcIj5UZXh0PC9kaXY+Jyk7XG4gICAgICAgIH0pO1xuICAgICAgICBpdChcInNhbml0aXplU3RyaW5nOiBzdmcgb25sb2FkLCBoZXgg0LIg0YLQtdCz0LVcIiwgKCkgPT4ge1xuICAgICAgICAgICAgY29uc3QgcGF5bG9hZCA9ICc8cyYjeDc2O2cgb25sb2FkPVwiYWxlcnQoMSlcIj48L3MmI3g3NjtnPic7XG4gICAgICAgICAgICAvLyBvbmxvYWQg0YPQtNCw0LvRj9C10YLRgdGPIERBTkdFUk9VU19QQVRURVJOUy5cbiAgICAgICAgICAgIC8vIDxzdmc+INC90LUg0YDQsNC30YDQtdGI0LXQvSAtPiDQutC+0LTQuNGA0YPQtdGC0YHRjy5cbiAgICAgICAgICAgIGV4cGVjdChkZWZlbmRlci5zYW5pdGl6ZVN0cmluZyhwYXlsb2FkKSkudG9CZShcIiZsdDtzdmcgJmd0OyZsdDsvc3ZnJmd0O1wiKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6IGlmcmFtZSBzcmMg0YEgVEFCINC4IEpTIFVSTFwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxJRlJBTUVcXHRzcmM9XCJqYXZhc2NyaXB0OmFsZXJ0KDEpXCI+PC9JRlJBTUU+JztcbiAgICAgICAgICAgIC8vIGphdmFzY3JpcHQ6INGD0LTQsNC70Y/QtdGC0YHRjyBEQU5HRVJPVVNfUEFUVEVSTlMuXG4gICAgICAgICAgICAvLyA8aWZyYW1lPiDQvdC1INGA0LDQt9GA0LXRiNC10L0gLT4g0LrQvtC00LjRgNGD0LXRgtGB0Y8uXG4gICAgICAgICAgICBleHBlY3QoZGVmZW5kZXIuc2FuaXRpemVTdHJpbmcocGF5bG9hZCkpLnRvQmUoJyZsdDtJRlJBTUVcXHRzcmM9XCJcIiZndDsmbHQ7L0lGUkFNRSZndDsnKTtcbiAgICAgICAgfSk7XG4gICAgICAgIGl0KFwic2FuaXRpemVTdHJpbmc6IGRhdGE6IFVSTCDQsiBocmVmLCBVbmljb2RlINCyINC/0YDQvtGC0L7QutC+0LvQtVwiLCAoKSA9PiB7XG4gICAgICAgICAgICBjb25zdCBwYXlsb2FkID0gJzxhIGhSZWY9XCJkXFx1MDA2MVRhOnRleHQvaHRtbCw8c2NyaXB0PmFsZXJ0KDEpPC9zY3JpcHQ+XCI+WDwvYT4nO1xuICAgICAgICAgICAgLy8gZGF0YTp0ZXh0L2h0bWwg0YHQviA8c2NyaXB0PiDRg9C00LDQu9GP0LXRgtGB0Y8g0LjQtyBocmVmIERBTkdFUk9VU19QQVRURVJOU1xuICAgICAgICAgICAgZXhwZWN0KGRlZmVuZGVyLnNhbml0aXplU3RyaW5nKHBheWxvYWQ