T09 · Insecure Skill Coding Practices
Warning
- Location
- static/index.html:253
- Finding
- DOM-based cross-site scripting through unsanitized transcript rendering<![CDATA[ ## Vulnerability Details **File Location**: `static/index.html:253-260` **Vulnerability Type**: DOM-based cross-site scripting **Risk Level**: Medium ### Vulnerable Code ```javascript function addTranscript(role, text) { const el = document.getElementById("transcript"); const line = document.createElement("div"); line.className = "transcript-line"; line.innerHTML = `<span class="role ${role}">${role === "user" ? "You" : "Agent"}:</span>${text}`; el.appendChild(line); el.scrollTop = el.scrollHeight; } ``` ### Technical Analysis The `text` value is concatenated directly into `innerHTML`. It originates from WebSocket transcript messages containing either speech-to-text output or the OpenClaw gateway's generated response. Neither the server nor the browser sanitizes it before HTML parsing. An HTML payload containing an executable event handler, such as an image with an `onerror` attribute, will therefore be interpreted as markup instead of displayed as text. The system prompt's request that the model avoid formatting is not a security boundary and does not prevent a model, external tool result, or manipulated response from returning HTML. The `role` field is also interpolated into an HTML attribute. Although the current server supplies fixed roles, the client should not assume WebSocket message contents are intrinsically safe. ### Attack Path 1. A user or malicious data source causes the OpenClaw agent to return attacker-controlled HTML, for example by requesting that it repeat a crafted string exactly. 2. The server forwards the response in a transcript WebSocket message: `{"type":"transcript","role":"assistant","text":"<img src=x onerror='...'>"}`. 3. `addTranscript()` inserts the value through `innerHTML`. 4. The browser parses the injected element and executes its event handler in the voice assistant's origin. 5. The injected JavaScript can read displayed conversation content, alter the interface, make same-origin req ...[truncated 741 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Construct the transcript exclusively with DOM text nodes and fixed, validated role values: ```javascript function addTranscript(role, text) { const el = document.getElementById("transcript"); const line = document.createElement("div"); line.className = "transcript-line"; const roleSpan = document.createElement("span"); const safeRole = role === "user" ? "user" : "assistant"; roleSpan.className = `role ${safeRole}`; roleSpan.textContent = safeRole === "user" ? "You: " : "Agent: "; line.appendChild(roleSpan); line.appendChild(document.createTextNode(String(text))); el.appendChild(line); el.scrollTop = el.scrollHeight; } ``` Additionally: - Validate WebSocket message schemas and reject unexpected roles or non-string transcript fields. - Deploy a restrictive Content Security Policy, particularly without `unsafe-inline`. - Avoid using model instructions as a substitute for output encoding. - If formatted assistant output is required later, process it with a maintained HTML sanitizer using a minimal allowlist. ]]>
