T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/token-usage-button.iife.js:104
- Finding
- Stored DOM XSS Through Unescaped Agent Names<![CDATA[ ## Vulnerability Details **File Location**: `scripts/token-usage-button.iife.js`, lines 104–116 and 166–170 **Vulnerability Type**: Stored DOM-based cross-site scripting / HTML injection **Risk Level**: Medium ### Vulnerable Code ```javascript const rows = agents.map((a) => { const pct = Math.round(((a.total || 0) / max) * 100); const bar = `<span class="milly-tk-bar" style="width:${Math.max(2, pct * 0.6)}px"></span>`; return ` <tr> <td>${a.agent}${bar}</td> <td>${a.calls || 0}</td> <td>${fmt(a.input)}</td> <td>${fmt(a.output)}</td> <td>${fmt(a.cacheRead)}</td> <td>${fmt(a.cacheWrite)}</td> <td><b>${fmt(a.total)}</b></td> <td>${fmt(a.billable)}</td> </tr>`; }).join(""); ``` The generated markup is subsequently assigned to `innerHTML`: ```javascript const resp = await fetch(DATA_URL + "?t=" + Date.now(), { cache: "no-cache" }); if (!resp.ok) throw new Error("HTTP " + resp.status + " — " + DATA_URL); const data = await resp.json(); body.innerHTML = render(data); ``` ### Technical Analysis The `a.agent` value is not a trusted constant. The Python aggregator derives it from a directory name beneath `~/.openclaw/agents` and includes it directly in the generated JSON: ```python agent = path.split(os.sep + "agents" + os.sep)[1].split(os.sep)[0] ``` The browser payload interpolates this value into an HTML template without escaping and assigns the result to `body.innerHTML`. Consequently, markup contained in an agent directory name or in a tampered `agent-token-usage.json` file is interpreted as HTML rather than displayed as text. A malicious value can inject arbitrary elements into the token-usage modal. Depending on the Control UI Content Security Policy and browser enforcement, an event-handler or equivalent browser-compatible payload may execute JavaScript. Even where CSP blocks script execution, arbitrary HTML injection remains possible and can be used for UI spoofing, misleading link ...[truncated 2318 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Do not render fetched data with `innerHTML`.** Construct the table using DOM APIs and assign all external values through `textContent`: ```javascript const td = document.createElement("td"); td.textContent = String(a.agent ?? ""); row.appendChild(td); ``` 2. **Separate trusted presentation markup from untrusted values.** Static elements such as the usage bar may be created with `document.createElement()` and styled through validated numeric properties. 3. **Validate the JSON schema before rendering.** - Require `agent`, `date`, and model names to be strings. - Require token and call values to be finite, non-negative numbers. - Reject unexpected properties or malformed records. - Apply reasonable length limits to names. 4. **Validate agent names in the Python aggregator.** Prefer deriving agent identifiers with `os.path.relpath()` and reject names containing control characters or markup-significant characters. This is defense in depth and must not replace safe browser rendering. 5. **If HTML templates are retained, apply context-appropriate escaping** to every JSON-derived string before interpolation. Escaping should cover at least `&`, `<`, `>`, `"`, and `'`. A maintained sanitizer may be used only when intentional HTML support is required. 6. **Apply a restrictive Control UI CSP** that disallows inline script, inline event handlers, `javascript:` navigation, and unauthorized network destinations. CSP should be treated as defense in depth rather than the primary fix. 7. **Protect the generated data file.** Ensure it is writable only by the intended user or service and served only through the authenticated Control UI. ]]>
