/** * Tests for security/xss-protection.js * * Tests HTML escaping, sanitization, and safe DOM manipulation * functions that protect against XSS attacks. */ import '@js/security/xss-protection.js'; const { escapeHtml, escapeHtmlAttribute, sanitizeUserInput, safeSetInnerHTML, safeSetTextContent } = window; describe('escapeHtml', () => { it('escapes ampersands', () => { expect(escapeHtml('a & b')).toBe('a & b'); }); it('escapes angle brackets', () => { expect(escapeHtml('')).toBe( '<script>alert(1)</script>' ); }); it('escapes double quotes', () => { expect(escapeHtml('"hello"')).toBe('"hello"'); }); it('escapes single quotes', () => { expect(escapeHtml("it's")).toBe('it's'); }); it('escapes forward slashes', () => { expect(escapeHtml('a/b')).toBe('a/b'); }); it('returns empty string for null', () => { expect(escapeHtml(null)).toBe(''); }); it('returns empty string for undefined', () => { expect(escapeHtml(undefined)).toBe(''); }); it('converts non-strings to string first', () => { expect(escapeHtml(123)).toBe('123'); expect(escapeHtml(true)).toBe('true'); }); it('handles strings with no special characters', () => { expect(escapeHtml('hello world')).toBe('hello world'); }); it('handles empty string', () => { expect(escapeHtml('')).toBe(''); }); it('escapes all dangerous characters together', () => { expect(escapeHtml('')).toBe( '<img onerror="alert('xss')" />' ); }); }); describe('escapeHtmlAttribute', () => { it('escapes double quotes for attribute context', () => { expect(escapeHtmlAttribute('value"with"quotes')).toContain('"'); }); it('escapes angle brackets', () => { expect(escapeHtmlAttribute('')).toContain('<'); }); it('converts non-strings', () => { expect(escapeHtmlAttribute(42)).toBe('42'); }); }); describe('sanitizeUserInput', () => { it('returns empty string for null/undefined', () => { expect(sanitizeUserInput(null)).toBe(''); expect(sanitizeUserInput(undefined)).toBe(''); }); it('trims whitespace by default', () => { const result = sanitizeUserInput(' hello '); expect(result).not.toMatch(/^\s/); expect(result).not.toMatch(/\s$/); }); it('does not trim when option is false', () => { const result = sanitizeUserInput(' hello ', { trimWhitespace: false }); // The result is escaped, so check it contains the text expect(result).toContain('hello'); }); it('enforces maxLength', () => { const long = 'a'.repeat(200); const result = sanitizeUserInput(long, { maxLength: 10 }); // After escaping, the core content is truncated to 10 chars expect(result.length).toBeLessThanOrEqual(10); }); it('normalizes line breaks', () => { const input = 'line1\r\nline2\rline3'; const result = sanitizeUserInput(input, { allowLineBreaks: true }); expect(result).not.toContain('\r'); }); it('escapes HTML by default (allowHtml=false)', () => { const result = sanitizeUserInput(''); expect(result).toContain('<'); expect(result).not.toContain('', '', '">', '&', '/', '`', '${', 'data:text/html,', '', '', ]; // Build a random combination const len = Math.floor(Math.random() * 5) + 1; let payload = ''; for (let i = 0; i < len; i++) { payload += fragments[Math.floor(Math.random() * fragments.length)]; // Insert random characters between const noise = Math.floor(Math.random() * 5); for (let j = 0; j < noise; j++) { payload += String.fromCharCode(Math.floor(Math.random() * 95) + 32); } } return payload; }; it('escaped output never contains raw < or > (200 random payloads)', () => { for (let i = 0; i < 200; i++) { const payload = generatePayload(); const escaped = escapeHtml(payload); expect(escaped).not.toMatch(//); } }); it('escaped output never contains raw double quotes', () => { for (let i = 0; i < 200; i++) { const payload = generatePayload(); const escaped = escapeHtml(payload); expect(escaped).not.toMatch(/"/); } }); it('escaped output is parseable as text content (round-trips through DOM)', () => { // If escaping works, the escaped string parsed as innerHTML and read // back via textContent should equal the original input. const div = document.createElement('div'); for (let i = 0; i < 100; i++) { const payload = generatePayload(); const escaped = escapeHtml(payload); // eslint-disable-next-line no-unsanitized/property -- test code div.innerHTML = escaped; // textContent decodes the entities back to the original expect(div.textContent).toBe(payload); // And no script/element tags were created expect(div.children.length).toBe(0); } }); it('escaped output never creates executable elements', () => { const div = document.createElement('div'); // Specific known XSS vectors const vectors = [ '', '', '', '">', '