From fbe4faaeb5ae345998cd99082499bcecff7d4375 Mon Sep 17 00:00:00 2001 From: Luka Zhang Date: Mon, 26 Jan 2026 18:23:24 -0800 Subject: [PATCH 1/2] Security: fix XSS vulnerability in Canvas Host debug page --- src/canvas-host/server.test.ts | 33 +++++++++++++++++++++++++++++++++ src/canvas-host/server.ts | 17 ++++++++++++----- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index f51e2f5e0..e216c07a4 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -229,4 +229,37 @@ describe("canvas host", () => { await fs.rm(dir, { recursive: true, force: true }); } }); + + it("default index.html uses safe DOM manipulation to prevent XSS", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-canvas-")); + + const server = await startCanvasHost({ + runtime: defaultRuntime, + rootDir: dir, + port: 0, + listenHost: "127.0.0.1", + allowInTests: true, + liveReload: false, + }); + + try { + const res = await fetch(`http://127.0.0.1:${server.port}${CANVAS_HOST_PATH}/`); + const html = await res.text(); + expect(res.status).toBe(200); + + // Verify the HTML uses safe DOM manipulation methods + expect(html).toContain("document.createElement"); + expect(html).toContain("textContent"); + + // Verify dangerous innerHTML assignment for status is not present + expect(html).not.toContain("statusEl.innerHTML"); + + // Verify the status element construction is present + expect(html).toContain('bridgeSpan.className'); + expect(html).toContain('bridgeSpan.textContent'); + } finally { + await server.close(); + await fs.rm(dir, { recursive: true, force: true }); + } + }); }); diff --git a/src/canvas-host/server.ts b/src/canvas-host/server.ts index b04d0d5ff..88bff81f9 100644 --- a/src/canvas-host/server.ts +++ b/src/canvas-host/server.ts @@ -100,11 +100,18 @@ function defaultIndexHTML() { const hasIOS = () => !!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.clawdbotCanvasA2UIAction); const hasAndroid = () => !!(window.clawdbotCanvasA2UIAction && typeof window.clawdbotCanvasA2UIAction.postMessage === "function"); const hasHelper = () => typeof window.clawdbotSendUserAction === "function"; - statusEl.innerHTML = - "Bridge: " + - (hasHelper() ? "ready" : "missing") + - " · iOS=" + (hasIOS() ? "yes" : "no") + - " · Android=" + (hasAndroid() ? "yes" : "no"); + + // Build status message safely using DOM manipulation to prevent XSS + statusEl.textContent = ""; // Clear existing content + statusEl.appendChild(document.createTextNode("Bridge: ")); + + const bridgeSpan = document.createElement("span"); + bridgeSpan.className = hasHelper() ? "ok" : "bad"; + bridgeSpan.textContent = hasHelper() ? "ready" : "missing"; + statusEl.appendChild(bridgeSpan); + + statusEl.appendChild(document.createTextNode(" · iOS=" + (hasIOS() ? "yes" : "no"))); + statusEl.appendChild(document.createTextNode(" · Android=" + (hasAndroid() ? "yes" : "no"))); window.addEventListener("clawdbot:a2ui-action-status", (ev) => { const d = ev && ev.detail || {}; From 80a9d67707ceee7f843a3af41aa6d7713c18b9d8 Mon Sep 17 00:00:00 2001 From: Luka Zhang Date: Mon, 26 Jan 2026 18:30:05 -0800 Subject: [PATCH 2/2] Fix: correct quote style in canvas-host test --- src/canvas-host/server.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index e216c07a4..4dc53ca00 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -255,8 +255,8 @@ describe("canvas host", () => { expect(html).not.toContain("statusEl.innerHTML"); // Verify the status element construction is present - expect(html).toContain('bridgeSpan.className'); - expect(html).toContain('bridgeSpan.textContent'); + expect(html).toContain("bridgeSpan.className"); + expect(html).toContain("bridgeSpan.textContent"); } finally { await server.close(); await fs.rm(dir, { recursive: true, force: true });