mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
fix(security): harden remaining inline onclick handlers against XSS double-context
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1385,11 +1385,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
<span class="case-manage-path">${escapeHtml(pathDisplay)}</span>
|
||||
</div>
|
||||
<div class="case-manage-actions">
|
||||
<button class="case-manage-btn" onclick="app.moveCaseUp('${escapeHtml(c.name)}')"
|
||||
<button class="case-manage-btn" onclick="app.moveCaseUp(${escapeHtml(JSON.stringify(c.name))})"
|
||||
title="Move up" ${isFirst ? 'disabled' : ''}>▲</button>
|
||||
<button class="case-manage-btn" onclick="app.moveCaseDown('${escapeHtml(c.name)}')"
|
||||
<button class="case-manage-btn" onclick="app.moveCaseDown(${escapeHtml(JSON.stringify(c.name))})"
|
||||
title="Move down" ${isLast ? 'disabled' : ''}>▼</button>
|
||||
<button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase('${escapeHtml(c.name)}')"
|
||||
<button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase(${escapeHtml(JSON.stringify(c.name))})"
|
||||
title="Delete case">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1484,14 +1484,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
const isSelected = c.name === currentCase;
|
||||
html += `
|
||||
<button class="mobile-case-item ${isSelected ? 'selected' : ''}"
|
||||
onclick="app.selectMobileCase('${escapeHtml(c.name)}')">
|
||||
onclick="app.selectMobileCase(${escapeHtml(JSON.stringify(c.name))})">
|
||||
<span class="mobile-case-item-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="mobile-case-item-name">${escapeHtml(c.name)}</span>
|
||||
<span class="mobile-case-item-delete" onclick="event.stopPropagation(); app.deleteCaseMobile('${escapeHtml(c.name)}')" title="Delete">
|
||||
<span class="mobile-case-item-delete" onclick="event.stopPropagation(); app.deleteCaseMobile(${escapeHtml(JSON.stringify(c.name))})" title="Delete">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user