mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-19 19:30:29 +08:00
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:
parent
607ab02b1f
commit
a03d63cba0
@ -519,14 +519,16 @@ function renderControlPaneHtml() {
|
||||
const blocker = item.blocker || (item.metadata && item.metadata.blocker) || '';
|
||||
const assigneeKind = item.assigneeKind || 'unassigned';
|
||||
const owner = item.assignee || item.owner || (assigneeKind === 'unassigned' ? 'unassigned (JIT)' : item.source) || 'unassigned';
|
||||
const idJs = "'" + String(item.id).replace(/'/g, "\\'") + "'";
|
||||
const moveButtons = ['ready', 'running', 'blocked', 'done'].map(lane => {
|
||||
const call = 'eccMoveItem(' + idJs + ", '" + lane + "')";
|
||||
return '<button type="button" onclick="' + call + '">' + escapeHtml(lane) + '</button>';
|
||||
}).join('');
|
||||
// Ids/lanes go into HTML-escaped data-* attributes and are read back via
|
||||
// dataset in delegated listeners below — never concatenated into inline
|
||||
// JS handlers — so a crafted work-item id cannot inject script (XSS).
|
||||
const idAttr = escapeHtml(item.id);
|
||||
const moveButtons = ['ready', 'running', 'blocked', 'done'].map(lane =>
|
||||
'<button type="button" data-wi-action="move" data-wi-id="' + idAttr + '" data-wi-lane="' + lane + '">' + escapeHtml(lane) + '</button>'
|
||||
).join('');
|
||||
const controls = state.allowActions
|
||||
? '<div class="row">'
|
||||
+ (assigneeKind === 'unassigned' ? '<button type="button" onclick="eccClaimItem(' + idJs + ')">Claim</button>' : '')
|
||||
+ (assigneeKind === 'unassigned' ? '<button type="button" data-wi-action="claim" data-wi-id="' + idAttr + '">Claim</button>' : '')
|
||||
+ moveButtons
|
||||
+ '</div>'
|
||||
: '';
|
||||
@ -539,6 +541,17 @@ function renderControlPaneHtml() {
|
||||
controls +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
document.querySelectorAll('#work-items [data-wi-action]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const id = button.getAttribute('data-wi-id');
|
||||
if (button.getAttribute('data-wi-action') === 'claim') {
|
||||
eccClaimItem(id);
|
||||
} else {
|
||||
eccMoveItem(id, button.getAttribute('data-wi-lane'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderKnowledge(knowledge) {
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user