diff --git a/common/src/util/__tests__/string.test.ts b/common/src/util/__tests__/string.test.ts index 3a141ca6b6..b1ea1c6964 100644 --- a/common/src/util/__tests__/string.test.ts +++ b/common/src/util/__tests__/string.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'bun:test' -import { pluralize } from '../string' +import { escapeHtml, escapeString, pluralize } from '../string' describe('pluralize', () => { it('should handle singular and plural cases correctly', () => { @@ -237,3 +237,38 @@ describe('pluralize', () => { }) }) + + +describe('escapeString', () => { + it('should escape JSON special characters', () => { + expect(escapeString('hello "world"')).toBe('hello \\"world\\"') + expect(escapeString('back\\slash')).toBe('back\\\\slash') + expect(escapeString('line\nbreak')).toBe('line\\nbreak') + }) + + it('should NOT escape HTML-unsafe characters (use escapeHtml for that)', () => { + // escapeString is for generic string escaping, not HTML contexts + expect(escapeString('')).toBe( + '\\u003cscript\\u003ealert(\\"xss\\")\\u003c/script\\u003e', + ) + expect(escapeHtml('a & b')).toBe('a \\u0026 b') + expect(escapeHtml("it's")).toBe('it\\u0027s') + expect(escapeHtml('5 > 3')).toBe('5 \\u003e 3') + }) + + it('should handle empty strings', () => { + expect(escapeHtml('')).toBe('') + }) + + it('should preserve regular characters', () => { + expect(escapeHtml('hello world')).toBe('hello world') + expect(escapeHtml('123')).toBe('123') + }) +}) diff --git a/common/src/util/string.ts b/common/src/util/string.ts index 506de962fd..9e78aac4ca 100644 --- a/common/src/util/string.ts +++ b/common/src/util/string.ts @@ -424,3 +424,17 @@ export function suffixPrefixOverlap(source: string, next: string): string { export const escapeString = (str: string) => { return JSON.stringify(str).slice(1, -1) } + +/** + * Escape characters that have special meaning in HTML/XML contexts to prevent + * XSS attacks and HTML injection. Use this when embedding user-controlled + * strings into HTML output, not for general string escaping (use escapeString). + */ +export const escapeHtml = (str: string): string => { + return JSON.stringify(str) + .slice(1, -1) + .replace(//g, '\\u003e') + .replace(/&/g, '\\u0026') + .replace(/'/g, '\\u0027') +}