fix(security): close XSS in control-pane board controls

The interactive claim/move buttons concatenated work-item ids into inline
onclick JS with only single-quote escaping — a crafted id (ids/titles come from
GitHub sync and manual upserts, not a strict allowlist) could break out and
inject script, even on the localhost-only server.

Fix: emit the id/lane in HTML-escaped data-* attributes (escapeHtml encodes
&<>"'), attach delegated click listeners that read them via getAttribute, and
pass the raw value as a JS string arg — never concatenated into code. Adds a
regression assertion that no inline onclick handlers with interpolated ids
remain. Flagged by automated security review.

Full suite 2845/2845; lint green.
This commit is contained in:
Affaan Mustafa
2026-06-18 18:25:28 -04:00
parent 607ab02b1f
commit a03d63cba0
2 changed files with 23 additions and 6 deletions
+4
View File
@@ -205,6 +205,10 @@ async function runTests() {
assert.ok(html.includes('function renderWorkItems'));
assert.ok(html.includes('function showError'));
assert.ok(html.includes('response.ok'));
// Board controls must use escaped data-* attributes + delegated
// listeners, never ids concatenated into inline onclick JS (XSS).
assert.ok(html.includes('data-wi-action'));
assert.ok(!/onclick="ecc(Claim|Move)Item\(/.test(html), 'no inline onclick handlers with interpolated ids');
const snapshot = await fetchLocal(`${app.url}/api/snapshot?query=control`).then(response => response.json());
assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1');