diff --git a/frontend/src/__tests__/apikeys.test.ts b/frontend/src/__tests__/apikeys.test.ts index 978bdd30c..3cc12f54b 100644 --- a/frontend/src/__tests__/apikeys.test.ts +++ b/frontend/src/__tests__/apikeys.test.ts @@ -289,6 +289,33 @@ describe('API Keys Module', () => { expect(api.revokeApiKey).toHaveBeenCalledWith('key-1'); }); + // Regression for ARCH-07 (issue #1195): the previous local escapeHtml + // copy (div.textContent -> div.innerHTML round-trip) did not escape + // quote characters, so a key id containing a double quote interpolated + // into data-key-id="..." broke out of the attribute and injected a new + // attribute (here onmouseover) into the button markup. + test('issue #1195: double quote in key id cannot inject attributes', async () => { + const hostileId = 'key-1" onmouseover="alert(1)'; + const mockKeys = [ + { + id: hostileId, + name: 'Hostile Key', + key_prefix: 'abc123', + is_active: true, + created_at: '2024-01-15T10:00:00Z' + } + ]; + + (api.getApiKeys as jest.Mock).mockResolvedValue({ api_keys: mockKeys }); + + await loadApiKeys(); + + const deleteBtn = document.querySelector('.delete-key-btn'); + expect(deleteBtn).not.toBeNull(); + expect(deleteBtn?.getAttribute('onmouseover')).toBeNull(); + expect(deleteBtn?.getAttribute('data-key-id')).toBe(hostileId); + }); + test('adds event listener to delete buttons', async () => { const mockKeys = [ { diff --git a/frontend/src/__tests__/users.test.ts b/frontend/src/__tests__/users.test.ts index f623080fd..8fa65e3f5 100644 --- a/frontend/src/__tests__/users.test.ts +++ b/frontend/src/__tests__/users.test.ts @@ -122,7 +122,7 @@ describe('users/utils', () => { describe('escapeHtml', () => { it('should escape HTML special characters', () => { expect(userUtils.escapeHtml('')).toBe( - '<script>alert("xss")</script>' + '<script>alert("xss")</script>' ); }); @@ -134,14 +134,28 @@ describe('users/utils', () => { expect(userUtils.escapeHtml('Tom & Jerry')).toBe('Tom & Jerry'); }); + // Regression for ARCH-07 (issue #1195): the previous local DOM + // round-trip implementation left both quote characters unescaped, + // so values interpolated inside double-quoted attributes (e.g. the + // aria-label carrying the user email in userList.ts) could break + // out of the attribute and inject markup. it('should escape single quotes', () => { const result = userUtils.escapeHtml("It's a test"); - expect(result).toBe("It's a test"); + expect(result).toBe('It's a test'); }); it('should escape double quotes', () => { const result = userUtils.escapeHtml('Say "Hello"'); - expect(result).toBe('Say "Hello"'); + expect(result).toBe('Say "Hello"'); + }); + + it('issue #1195: quote in attribute position cannot break out of the attribute', () => { + const malicious = 'a@b.com" onmouseover="alert(1)'; + const div = document.createElement('div'); + div.innerHTML = ``; + const btn = div.querySelector('button'); + expect(btn?.getAttribute('onmouseover')).toBeNull(); + expect(btn?.getAttribute('aria-label')).toBe(malicious); }); it('should handle empty string', () => { @@ -150,7 +164,7 @@ describe('users/utils', () => { it('should handle multiple special characters', () => { expect(userUtils.escapeHtml('