Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
100 changes: 13 additions & 87 deletions backend/src/sanitizer.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,35 @@
/**
* 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<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
};
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
*/
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 = [
'<script',
'javascript:',
'onerror=',
'onclick=',
'onload=',
'<iframe',
'eval(',
'expression(',
];

return patterns.some(p => 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: '<b>bold</b>',
scriptTag: '<script>alert(1)</script>',
imgTag: '<img src=x onerror=alert(1)>',

// Should be encoded
angleBrackets: '<script>alert(1)</script>',
quotes: '"test"',
apostrophe: "'test'",

// Should remain unchanged (already safe)
plainText: 'This is a plain text report',
specialChars: 'Test & valid <report>',
};

// Export for tests
export default {
sanitizeInput,
containsXss,
XSS_TEST_CASES,
};
export default { sanitizeInput, containsXss };