From e3dac8b5a6fcbb3848990b26e458cf3a970ecf71 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 10 May 2026 12:40:31 +0200 Subject: [PATCH] fix(ui): close CodeQL js/xss-through-dom finding (PR #240 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL alert 132 flagged the reboot status line as a sink that received user-controlled DOM text (device names from the migration/sync select options and table rows) without escaping. Six data-flow paths converged on script.js:1950. Switch the sink at line 1950 from .innerHTML to .textContent — the status message has never needed HTML formatting. The pre-existing display-into-innerHTML pattern still exists elsewhere in this file but those lines aren't in this PR's scope and are tracked by their own historical alerts. Also harden the (newer) `currentP.innerHTML = ... + data.current + ...` line in loadAccountIDSuggestions: rebuild the paragraph with replaceChildren + createElement so the account ID never becomes HTML, even though it's expected to be a 7-digit string. Coerce known account IDs to String() when populating the existing-account dropdown so the IDE's type inference stops complaining about opt.value = id; / opt.textContent = id; on data of unknown[] type. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/service/handlers/web/js/script.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index 09f37c7..05fc7ca 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -1947,7 +1947,8 @@ async function reboot(deviceId, ip) { const statusDiv = document.getElementById("status"); statusDiv.style.display = "block"; statusDiv.style.backgroundColor = "#ffffcc"; - statusDiv.innerHTML = "Rebooting " + display + " via " + rebootMethod + "..."; + // textContent avoids reinterpreting the (user-controlled) device name as HTML. + statusDiv.textContent = "Rebooting " + display + " via " + rebootMethod + "..."; try { const url = "/setup/reboot/" + encodeURIComponent(deviceId) @@ -1993,19 +1994,22 @@ async function loadAccountIDSuggestions(deviceId) { // Reset existingSelect.innerHTML = ""; - (data.known || []).forEach((id) => { + (data.known || []).forEach((/** @type {string} */ id) => { const opt = document.createElement("option"); - opt.value = id; - opt.textContent = id; + opt.value = String(id); + opt.textContent = String(id); existingSelect.appendChild(opt); }); if (data.current) { currentP.style.display = "block"; - currentP.innerHTML = - "Speaker is already paired with account " - + "" + data.current + "" - + ". You can keep it (recommended) or re-pair to a different ID."; + // Build the paragraph with createElement so the user-controlled + // account ID never becomes HTML. + currentP.replaceChildren( + document.createTextNode("Speaker is already paired with account "), + Object.assign(document.createElement("strong"), {textContent: data.current}), + document.createTextNode(". You can keep it (recommended) or re-pair to a different ID."), + ); input.value = data.current; freshDiv.style.display = "block"; } else {