fix(security): prevent XSS in WebGL error toast by using DOM API

- Refactor `showWebGLError` to use `document.createElement` instead of `innerHTML`
- Replace inline `onclick` handler with `addEventListener` for CSP compliance
- Add unit test to verify XSS prevention and button functionality
This commit is contained in:
google-labs-jules[bot]
2026-01-18 04:50:44 +00:00
parent 371f4b49b7
commit dd4e34df6f
4 changed files with 108 additions and 29 deletions
+29 -14
View File
@@ -403,21 +403,36 @@ export class WebGLRenderer {
animation: slideIn 0.3s ease-out;
`;
toast.innerHTML = `
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="font-size: 24px;">⚠️</span>
<div>
<strong style="display: block; margin-bottom: 6px;">MagnifyGlass: WebGL Disabled</strong>
<span style="opacity: 0.9;">${instructions}</span>
<button onclick="this.parentElement.parentElement.parentElement.remove()"
style="display: block; margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2);
const container = document.createElement('div');
container.style.cssText = "display: flex; align-items: flex-start; gap: 12px;";
const icon = document.createElement('span');
icon.style.fontSize = "24px";
icon.textContent = "⚠️";
container.appendChild(icon);
const content = document.createElement('div');
const title = document.createElement('strong');
title.style.cssText = "display: block; margin-bottom: 6px;";
title.textContent = "MagnifyGlass: WebGL Disabled";
content.appendChild(title);
const message = document.createElement('span');
message.style.opacity = "0.9";
message.textContent = instructions;
content.appendChild(message);
const btn = document.createElement('button');
btn.textContent = "Dismiss";
btn.style.cssText = `display: block; margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; color: white;
cursor: pointer; font-size: 12px;">
Dismiss
</button>
</div>
</div>
`;
cursor: pointer; font-size: 12px;`;
btn.addEventListener('click', () => toast.remove());
content.appendChild(btn);
container.appendChild(content);
toast.appendChild(container);
// Add animation keyframes
if (!document.getElementById('magnifyglass-toast-style')) {
+55
View File
@@ -0,0 +1,55 @@
import { describe, it, expect, afterEach, vi } from 'vitest';
import { WebGLRenderer } from '../../src/magnify-glass/WebGLRenderer';
describe('WebGLRenderer Security', () => {
afterEach(() => {
document.body.innerHTML = '';
vi.restoreAllMocks();
});
it('should safely render error instructions without XSS', () => {
// Mock dependencies
const mockUi = { glassCanvas: null } as any;
const mockConfig = {} as any;
const mockState = {} as any;
const renderer = new WebGLRenderer(mockConfig, mockState, mockUi);
// Payload with XSS vector
const maliciousInstruction = 'Failed <img src=x onerror=alert(1)>';
renderer.showWebGLError(maliciousInstruction);
const toast = document.getElementById('magnifyglass-webgl-error');
expect(toast).toBeTruthy();
// Check if the malicious instruction is rendered as text, not HTML
// In the vulnerable version, this would be true:
// expect(toast?.innerHTML).toContain('<img src=x onerror=alert(1)>');
// In the fixed version, we expect:
expect(toast?.textContent).toContain(maliciousInstruction);
expect(toast?.innerHTML).not.toContain('<img src=x onerror=alert(1)>');
expect(toast?.innerHTML).toContain('&lt;img src=x onerror=alert(1)&gt;');
});
it('should have a working dismiss button using event listener', () => {
const mockUi = { glassCanvas: null } as any;
const renderer = new WebGLRenderer({} as any, {} as any, mockUi);
renderer.showWebGLError('Test error');
const toast = document.getElementById('magnifyglass-webgl-error');
const button = toast?.querySelector('button');
expect(button).toBeTruthy();
// Verify inline handler is NOT present (CSP compliance)
expect(button?.hasAttribute('onclick')).toBe(false);
// Click to dismiss
button?.click();
// Verify toast is removed
expect(document.getElementById('magnifyglass-webgl-error')).toBeNull();
});
});
+23 -14
View File
@@ -290,21 +290,30 @@ class WebGLRenderer {
z-index: 100000;
animation: slideIn 0.3s ease-out;
`;
toast.innerHTML = `
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="font-size: 24px;">⚠️</span>
<div>
<strong style="display: block; margin-bottom: 6px;">MagnifyGlass: WebGL Disabled</strong>
<span style="opacity: 0.9;">${instructions}</span>
<button onclick="this.parentElement.parentElement.parentElement.remove()"
style="display: block; margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2);
const container = document.createElement("div");
container.style.cssText = "display: flex; align-items: flex-start; gap: 12px;";
const icon = document.createElement("span");
icon.style.fontSize = "24px";
icon.textContent = "⚠️";
container.appendChild(icon);
const content = document.createElement("div");
const title = document.createElement("strong");
title.style.cssText = "display: block; margin-bottom: 6px;";
title.textContent = "MagnifyGlass: WebGL Disabled";
content.appendChild(title);
const message = document.createElement("span");
message.style.opacity = "0.9";
message.textContent = instructions;
content.appendChild(message);
const btn = document.createElement("button");
btn.textContent = "Dismiss";
btn.style.cssText = `display: block; margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; color: white;
cursor: pointer; font-size: 12px;">
Dismiss
</button>
</div>
</div>
`;
cursor: pointer; font-size: 12px;`;
btn.addEventListener("click", () => toast.remove());
content.appendChild(btn);
container.appendChild(content);
toast.appendChild(container);
if (!document.getElementById("magnifyglass-toast-style")) {
const style = document.createElement("style");
style.id = "magnifyglass-toast-style";
File diff suppressed because one or more lines are too long