From a8782b364fd43ad0b787101aedd7d40d03900c04 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 19 Jun 2026 15:26:42 +0200 Subject: [PATCH] fix(security): harden remaining inline onclick handlers against XSS double-context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends PR #132 (ultracode handlers) to the rest of the frontend. The same JS-string-in-HTML-attribute pattern — '${escapeHtml(value)}' — remained in 32 more inline handlers across app.js, panels-ui.js, session-ui.js, subagent-windows.js, and notification-manager.js. The browser HTML-decodes the attribute value before parsing the handler source, so escapeHtml's ' reverts to ' and a quote-bearing id/path/name breaks out of the JS string literal into executable code. Switch all to escapeHtml(JSON.stringify(value)): JSON.stringify JS-encodes and quote-wraps first, then escapeHtml handles the HTML-attribute layer, so the value round-trips as one inert string argument. Also fixes two non-escapeHtml variants of the same class: - panels-ui.js: mux-session `sid` was pre-escaped with escapeHtml() then dropped into a single-quoted JS string (selectSession / killMuxSession). Now JSON.stringify'd at the source. - orchestrator-panel.js: phase.id was interpolated raw (no escaping at all) into orchestratorSkipPhase / orchestratorRetryPhase. Now escapeHtml(JSON.stringify()). The most realistic vector here is file paths (panels-ui openLogViewerWindow) — filenames can legally contain a single quote. Numeric interpolations (${i+1}, ${index}, ${item.version}) and the developer-literal ${onclick} in orchestrator-panel are not user data and are left as-is. Verified: 0 vulnerable patterns remain, all 22 frontend files parse (check:frontend-syntax + node --check), and a runtime round-trip confirms the injection that fired under the old pattern is now an inert string argument. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/public/app.js | 8 +++--- src/web/public/notification-manager.js | 2 +- src/web/public/orchestrator-panel.js | 4 +-- src/web/public/panels-ui.js | 34 +++++++++++++------------- src/web/public/session-ui.js | 10 ++++---- src/web/public/subagent-windows.js | 8 +++--- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 63535778..e5153a25 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2782,7 +2782,7 @@ class CodemanApp { const tallTabsEnabled = this._tallTabsEnabled ?? false; const showFolder = tallTabsEnabled && session.name && folderName && folderName !== name; - parts.push(`
@@ -3275,14 +3275,14 @@ Object.assign(CodemanApp.prototype, { ${sizeKB} KB
- - + +
${escapeHtml(fileName)} + onclick="app.openImageInNewTab(${escapeHtml(JSON.stringify(imageUrl))})" />
`; @@ -3505,9 +3505,9 @@ Object.assign(CodemanApp.prototype, { modelHtml = `${modelShort}`; } - const sid = escapeHtml(muxSession.sessionId); + const sid = escapeHtml(JSON.stringify(muxSession.sessionId)); html += ` -
+
${statusLabel}
${modelHtml} ${escapeHtml(muxSession.name || muxSession.muxName)}
@@ -3520,7 +3520,7 @@ Object.assign(CodemanApp.prototype, {
- +
`; @@ -3563,7 +3563,7 @@ Object.assign(CodemanApp.prototype, {
- ${agent.status !== 'completed' ? `` : ''} + ${agent.status !== 'completed' ? `` : ''}
`; diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index 1602948d..2a2e07f7 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -1385,11 +1385,11 @@ Object.assign(CodemanApp.prototype, { ${escapeHtml(pathDisplay)}
- - -
@@ -1484,14 +1484,14 @@ Object.assign(CodemanApp.prototype, { const isSelected = c.name === currentCase; html += ` + ${parentHeader}