From 06871eb7e3f077169db2f6115e8b8ecbb0f95dd8 Mon Sep 17 00:00:00 2001 From: Aamer Akhter Date: Fri, 19 Jun 2026 08:55:24 -0400 Subject: [PATCH] Harden ultracode inline onclick handlers against XSS The ultracode run/agent cards and minimized-tab badges built inline onclick handlers by interpolating escapeHtml(value) inside single-quoted JavaScript strings within an HTML attribute: onclick="app.openUltracodeAgentWindow('${escapeHtml(agentId)}', ...)" escapeHtml maps ' -> ', but the browser HTML-decodes the attribute value before the handler source is parsed, so ' becomes a literal ' again and a quote in a run/agent/session id breaks out of the string literal into executable JS. escapeHtml alone is insufficient for the JS-string-within-HTML- attribute double context. Switch each handler to escapeHtml(JSON.stringify(value)): JSON.stringify JS-encodes and quote-wraps the value, then escapeHtml handles the HTML attribute layer, so the value round-trips as an inert string argument. This matches the encoding already used by other handlers in these files. Affected: - ultracode-panel.js: selectWorkflowRun, openUltracodeAgentWindow - ultracode-windows.js: restore/dismiss for minimized run and agent tabs --- src/web/public/ultracode-panel.js | 4 ++-- src/web/public/ultracode-windows.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/web/public/ultracode-panel.js b/src/web/public/ultracode-panel.js index 96d9c4a9..86f41eda 100644 --- a/src/web/public/ultracode-panel.js +++ b/src/web/public/ultracode-panel.js @@ -204,7 +204,7 @@ Object.assign(CodemanApp.prototype, { phasesHtml = `
${chips.join('')}
`; } return ( - `
` + + `
` + `
${name}` + `${escapeHtml(status || '—')}
` + `
${escapeHtml(stats)}
` + @@ -279,7 +279,7 @@ Object.assign(CodemanApp.prototype, { const cardStateCls = state === 'done' ? ' uw-state-done' : state === 'progress' ? ' uw-state-working' : ''; const cardAttrs = clickable ? ` class="ultracode-agent-card ultracode-agent-card--clickable${cardStateCls}" role="button" tabindex="0"` + - ` title="View transcript" onclick="app.openUltracodeAgentWindow('${escapeHtml(a.agentId)}','${escapeHtml(runId || '')}')"` + ` title="View transcript" onclick="app.openUltracodeAgentWindow(${escapeHtml(JSON.stringify(a.agentId))},${escapeHtml(JSON.stringify(runId || ''))})"` : ` class="ultracode-agent-card${cardStateCls}"`; return ( `` + diff --git a/src/web/public/ultracode-windows.js b/src/web/public/ultracode-windows.js index b7ef4637..0c0eb347 100644 --- a/src/web/public/ultracode-windows.js +++ b/src/web/public/ultracode-windows.js @@ -351,11 +351,11 @@ Object.assign(CodemanApp.prototype, { const name = run ? run.workflowName || run.summary || runId : runId; const statusCls = this._workflowStatusClass(run ? String(run.status || '') : ''); items.push( - `
` + + `
` + `` + `🧬` + `${escapeHtml(trunc(name))}` + - `×` + + `×` + `
` ); } @@ -365,11 +365,11 @@ Object.assign(CodemanApp.prototype, { for (const [agentId, entry] of agentMap) { const name = (entry && entry.label) || agentId; items.push( - `
` + + `
` + `` + `📄` + `${escapeHtml(trunc(name))}` + - `×` + + `×` + `
` ); }