From 416090376e2742f8fb257443191ac97d7fc79abd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:38:38 +0000 Subject: [PATCH] feat(security): sanitize user input in info panel to prevent XSS - Add `escapeHtml` utility to `src/shared/utils.ts` - Sanitize rendered values in `src/info-panel/ValueFormatter.ts` - Sanitize node titles, types, and labels in `src/info-panel/UIManager.ts` - Add unit tests verifying XSS prevention in `tests/unit/Security.test.ts` --- .jules/sentinel.md | 6 ++++++ src/info-panel/UIManager.ts | 21 +++++++++++---------- src/info-panel/ValueFormatter.ts | 8 +++++--- src/shared/utils.ts | 15 +++++++++++++++ tests/unit/Security.test.ts | 25 +++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 .jules/sentinel.md create mode 100644 tests/unit/Security.test.ts diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..c26da33 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,6 @@ +# Sentinel's Journal + +## 2026-01-06 - Improper DOM Manipulation XSS +**Vulnerability:** Found multiple instances of `innerHTML` being used with unsanitized user inputs (Node titles, widget values) in `UIManager.ts`. +**Learning:** This project uses direct DOM manipulation without a framework, making XSS a primary risk. Developers were manually building HTML strings. +**Prevention:** Introduced `escapeHtml` utility. Any new code using `innerHTML` MUST sanitize inputs. Prefer `textContent` where possible, or use the `escapeHtml` helper. diff --git a/src/info-panel/UIManager.ts b/src/info-panel/UIManager.ts index 46c67a4..dbe0af2 100644 --- a/src/info-panel/UIManager.ts +++ b/src/info-panel/UIManager.ts @@ -8,6 +8,7 @@ import { StateManager } from './StateManager'; import { Icons } from '../shared/icons'; import { Logger } from '../shared/logger'; +import { escapeHtml } from '../shared/utils'; import { formatValue, getValueClass, getValueAttributes, formatWidgetValue } from './ValueFormatter'; import { getCheckpointInfo, @@ -872,11 +873,11 @@ export class UIManager { } this.elements.content.innerHTML = sections.map(section => ` -
-
+
+
${section.icon} - ${section.title} - ${section.badge ? `${section.badge}` : ''} + ${escapeHtml(section.title)} + ${section.badge ? `${escapeHtml(section.badge)}` : ''} ${section.id !== 'node' ? `${Icons.chevronRight}` : ''}
@@ -894,7 +895,7 @@ export class UIManager { const rawValue = String(item.value || ''); const showCopyBtn = !item.clickable && rawValue.length > 3 && typeof item.value === 'string'; const copyBtnHtml = showCopyBtn ? ` - ` : ''; return `
- ${item.label} + ${escapeHtml(item.label)} ${value}${dropdownIcon} ${copyBtnHtml}
`; @@ -1126,13 +1127,13 @@ export class UIManager { const execNode = node as NodeExecOrderEntry; item.innerHTML = ` #${execNode.order} - ${execNode.title} (#${execNode.id}) - ${execNode.type} + ${escapeHtml(execNode.title)} (#${execNode.id}) + ${escapeHtml(execNode.type)} `; } else { item.innerHTML = ` - ${node.title} (#${node.id}) - ${node.type} + ${escapeHtml(node.title)} (#${node.id}) + ${escapeHtml(node.type)} `; } diff --git a/src/info-panel/ValueFormatter.ts b/src/info-panel/ValueFormatter.ts index fde7e11..a74e97e 100644 --- a/src/info-panel/ValueFormatter.ts +++ b/src/info-panel/ValueFormatter.ts @@ -5,6 +5,8 @@ * Extracted from UIManager.ts for better modularity. */ +import { escapeHtml } from '../shared/utils'; + /** * Format a value for display in the info panel. * @param value - The value to format @@ -26,11 +28,11 @@ export function formatValue(value: unknown, label?: string): string { label.toLowerCase().includes('positive') || label.toLowerCase().includes('negative') )) { - return str; + return escapeHtml(str); } // Show full text for very long values (no truncation) - return str; + return escapeHtml(str); } /** @@ -64,7 +66,7 @@ export function getValueAttributes(value: unknown): string { const str = String(value); if (str.length > 500) { // Only for extremely long text - return `title="${str.replace(/"/g, '"')}"`; + return `title="${escapeHtml(str)}"`; } return ''; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 30c1ed7..6c33ede 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -159,3 +159,18 @@ export function createDebugLogger( } }; } + +/** + * Escapes HTML special characters in a string to prevent XSS. + * @param str - The string to escape + * @returns Escaped string + */ +export function escapeHtml(str: string): string { + if (!str) return str; + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/tests/unit/Security.test.ts b/tests/unit/Security.test.ts new file mode 100644 index 0000000..ad3f258 --- /dev/null +++ b/tests/unit/Security.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { formatValue } from '../../src/info-panel/ValueFormatter'; + +describe('Security', () => { + describe('formatValue', () => { + it('should escape HTML in values to prevent XSS', () => { + const maliciousInput = ''; + const escaped = formatValue(maliciousInput); + + // Assert that the output does NOT contain the raw script tag + expect(escaped).not.toContain('