From ed74d590911c855a08dd7c2d7887a242b588e904 Mon Sep 17 00:00:00 2001 From: MATHEW Date: Mon, 1 Jun 2026 11:36:59 +0000 Subject: [PATCH] fix(backend): replace regex XSS detection with xss library (#642) Regex-based XSS detection in containsXss() and sanitizeInput() can be bypassed with HTML entity encoding, Unicode escapes, or obfuscation. Replace custom regex patterns with the battle-tested xss npm package (v1.0.15) which handles all known bypass vectors. --- backend/package-lock.json | 31 +++++++++++- backend/package.json | 3 +- backend/src/sanitizer.ts | 100 +++++--------------------------------- 3 files changed, 45 insertions(+), 89 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 144b05e5..a1065f3a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -29,7 +29,8 @@ "pg": "^8.11.0", "swagger-ui-express": "^5.0.0", "toml": "^3.0.0", - "uuid": "^14.0.0" + "uuid": "^14.0.0", + "xss": "^1.0.15" }, "devDependencies": { "@apidevtools/swagger-cli": "^4.0.4", @@ -3127,6 +3128,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -3219,6 +3226,12 @@ "node": ">= 8" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7171,6 +7184,22 @@ "dev": true, "license": "ISC" }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 8e269d0c..2ff9c034 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,7 +38,8 @@ "pg": "^8.11.0", "swagger-ui-express": "^5.0.0", "toml": "^3.0.0", - "uuid": "^14.0.0" + "uuid": "^14.0.0", + "xss": "^1.0.15" }, "overrides": { "uuid": "^14.0.0" diff --git a/backend/src/sanitizer.ts b/backend/src/sanitizer.ts index c5ebd3b2..42982b4f 100644 --- a/backend/src/sanitizer.ts +++ b/backend/src/sanitizer.ts @@ -1,27 +1,16 @@ /** * Input Sanitizer Module - * - * Provides XSS protection by sanitizing user input that may be rendered as HTML. - * Strips HTML tags and encodes special characters. + * + * Provides XSS protection by sanitizing user input using the `xss` library, + * which handles HTML entity encoding, Unicode escapes, and obfuscation that + * bypass regex-based detection. */ -/// HTML entities that need encoding -const HTML_ENTITIES: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', -}; +import xss from 'xss'; /** - * Sanitize a string to prevent XSS attacks - * - * This function: - * 1. Strips all HTML tags - * 2. Encodes HTML special characters - * + * Sanitize a string to prevent XSS attacks. + * * @param input - The raw user input * @returns Sanitized string safe for storage/display */ @@ -29,81 +18,18 @@ export function sanitizeInput(input: string): string { if (!input || typeof input !== 'string') { return ''; } - - let sanitized = input; - - // Step 1: Strip HTML tags using regex - // Removes <...> patterns including content between them - sanitized = sanitized.replace(/<[^>]*>/g, ''); - - // Remove script tag patterns (case insensitive) - sanitized = sanitized.replace(/script/gi, ''); - sanitized = sanitized.replace(/on\w+=/gi, ''); // Remove event handlers - - // Step 2: Encode HTML special characters - for (const [char, entity] of Object.entries(HTML_ENTITIES)) { - sanitized = sanitized.split(char).join(entity); - } - - // Step 3: Trim whitespace and collapse - sanitized = sanitized.trim(); - - // Remove multiple whitespaces - sanitized = sanitized.replace(/\s+/g, ' '); - - return sanitized; + return xss(input.trim()); } /** - * Check if input contains potential XSS threats - * + * Check if input contains potential XSS threats. + * * @param input - Input to check - * @returns True if suspicious content detected + * @returns True if the xss library would modify the string (i.e. suspicious content detected) */ export function containsXss(input: string): boolean { if (!input) return false; - - const lower = input.toLowerCase(); - - // Check for common XSS patterns - const patterns = [ - ' lower.includes(p)); + return xss(input) !== input; } -/** - * Test if sanitization neutralizes XSS payloads - * - * Test cases to verify: - */ -export const XSS_TEST_CASES = { - // Should be stripped - basicHtmlTag: 'bold', - scriptTag: '', - imgTag: '', - - // Should be encoded - angleBrackets: '', - quotes: '"test"', - apostrophe: "'test'", - - // Should remain unchanged (already safe) - plainText: 'This is a plain text report', - specialChars: 'Test & valid ', -}; - -// Export for tests -export default { - sanitizeInput, - containsXss, - XSS_TEST_CASES, -}; \ No newline at end of file +export default { sanitizeInput, containsXss };