diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts
index f51e2f5e0..4dc53ca00 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 || {};