// @vitest-environment jsdom /** * COD-56 — markdown HTML sanitizer (mXSS hardening). * * The response viewer / attachment preview render agent- and transcript-derived markdown to * HTML via `marked` (raw-HTML passthrough) and assign the result with innerHTML. The HTML must * be sanitized first. The original sanitizer (`_sanitizeHtml` in app.js) was a hand-rolled * DENYLIST and is mXSS-prone — it never stripped `svg`/`math`/`style`, so foreign-namespace and * CSS vectors survived. * * This suite drives the EXACT shipping artifacts under jsdom: * - src/web/public/vendor/dompurify.min.js (the vendored sanitizer) * - src/web/public/sanitize-html.js (our allowlist config wired to DOMPurify) * * It feeds a corpus of mXSS payloads (svg/math/style/namespace-confusion/event-handler) and * asserts the output carries NO script-executing constructs, and that legitimate * markdown-rendered HTML survives unchanged. * * RED demonstration: a faithful re-implementation of the OLD denylist sanitizer is included and * asserted to LET payloads through — the gap this fix closes. * * No port / server needed; pure DOM logic in jsdom. */ import { describe, it, expect, beforeAll } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const publicDir = join(process.cwd(), 'src/web/public'); /** Build the SHIPPING sanitizer the way the browser does: vendored DOMPurify + sanitize-html.js, * both evaluated inside the (jsdom) window so DOMPurify binds to a real DOM. */ function loadShippingSanitizer(): (html: string) => string { const win = window as unknown as Record; // Evaluate the vendored UMD in the jsdom global context (mirrors ' }, { name: 'svg/style mXSS', html: '' }, { name: 'math/mtext/table namespace confusion', html: '
', }, { name: 'style attr expression', html: '
x
' }, { name: 'style attr url(javascript:)', html: '
x
' }, { name: 'style element', html: '' }, { name: 'noscript wrap', html: '