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:
@@ -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')) {
|
||||
|
||||
@@ -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('<img src=x onerror=alert(1)>');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user