web-ui: sanitize markdown HTML with DOMPurify to fix XSS
Both mdNode implementations (agent UI app.js, dashboard common.js) assigned marked.parse() output straight to innerHTML with no sanitizer. marked v5+ dropped its built-in sanitize option, and there was no DOMPurify anywhere in frontend/, so markdown containing raw HTML/script tags rendered live in the browser. Both sinks receive untrusted input in practice: the agent UI's mdNode renders recv tool_result bodies, assistant prose, and send/ask/answer payloads sourced from peer agents and matrix-relayed messages (the documented prompt-injection adversary); the dashboard's mdNode renders agent-authored .md files served verbatim by GET /api/state-file (the endpoint validates path, not content). Since the per-agent UI and dashboard are same-origin behind the gateway with operator-authority endpoints (approve/spawn/rebuild/destroy/answer-question), injected script would run with the operator's session. Fix: DOMPurify.sanitize() the marked.parse() output at both sinks before assigning to innerHTML. Added dompurify as a dependency to both the agent and dashboard npm workspaces, recomputed npmDepsHash in nix/frontend.nix for the updated lockfile. Also corrected docs/web-ui/shape.md, which claimed the markdown-rendering path was XSS-safe by construction the same way the text-node-based linkify path is — it isn't; it's safe because it's sanitized. CSP hardening for the dashboard (no unsafe-inline) is a separate, larger backend change (response headers in hive-c0re) and is left as a fast-follow rather than folded into this fix.
This commit is contained in:
parent
9adf074504
commit
ccc5e631e2
7 changed files with 50 additions and 12 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { create as termCreate, linkify as termLinkify } from '@hive/shared/terminal.js';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Expose the previously-script-tag-provided globals so the IIFE below
|
||||
// keeps working unchanged. Pre-split these were attached by
|
||||
|
|
@ -1429,6 +1430,11 @@ window.marked = marked;
|
|||
// `.md` class (CSS in TERMINAL_CSS scopes paragraph/code/list
|
||||
// styles to it). Falls back to a plain text node if marked isn't
|
||||
// loaded (network glitch, asset 404) so the body still renders.
|
||||
// `text` is untrusted (peer-agent / matrix-relayed message bodies,
|
||||
// agent-authored files) — the parsed HTML is run through DOMPurify
|
||||
// before it ever touches innerHTML, since markdown can carry raw
|
||||
// HTML/script tags that `marked` itself no longer strips (v5+
|
||||
// dropped the built-in sanitizer).
|
||||
function mdNode(text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'md';
|
||||
|
|
@ -1436,7 +1442,7 @@ window.marked = marked;
|
|||
if (window.marked && typeof window.marked.parse === 'function') {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
div.innerHTML = marked.parse(src);
|
||||
div.innerHTML = DOMPurify.sanitize(marked.parse(src));
|
||||
// marked autolinks URLs but leaves them same-tab — open them
|
||||
// externally so a click never unloads the terminal.
|
||||
div.querySelectorAll('a[href]').forEach((a) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue