Skip to content

ZBBS-WORK-395: harden the admin mermaid note-render path against XSS#234

Merged
jeffdafoe merged 1 commit into
mainfrom
zbbs-work-395-admin-mermaid-xss-hardening
Jun 11, 2026
Merged

ZBBS-WORK-395: harden the admin mermaid note-render path against XSS#234
jeffdafoe merged 1 commit into
mainfrom
zbbs-work-395-admin-mermaid-xss-hardening

Conversation

@jeffdafoe

Copy link
Copy Markdown
Owner

Why

The notes viewer's mermaid path is the one v-html sink in the admin SPA fed from semi-trusted data (notes come from agents and the dream pipeline's LLM output). Its only sanitization was mermaid's implicit securityLevel: 'strict' default — a single point of failure with bypass CVEs in its history, one careless 'loose' tweak away from stored XSS, with no CSP behind it. Full audit in shared/tasks/pending/zbbs-work-395-admin-mermaid-xss-hardening (all other render surfaces verified framework-escaped).

What

In notes.js, three coupled changes:

  • Pin securityLevel: 'strict' so a regression is a visible diff line.
  • htmlLabels: false — labels become native SVG <text>. Required for the next piece: verified against the repo's exact dists in headless Firefox that with HTML labels on, the SVG sanitize profile destroys every flowchart label, and re-allowing foreignObject still loses its HTML children to DOMPurify's namespace rules.
  • App-level DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }) before the v-html assignment — belt-and-suspenders matching the markdown path.

Verified lossless on flowchart / sequence / adversarial-payload diagrams (zero element loss, labels intact, payloads inert as escaped text, DOM scan clean of on*/script/javascript:). vite build:admin passes. Frontend-only, no migration.

code_review: 1 round, no blocking issues. CSP for the admin app stays deferred (ticket item 3).

— Work

🤖 Generated with Claude Code

The notes viewer's mermaid path was the one v-html sink fed from
semi-trusted data (notes are authored by agents and the dream
pipeline's LLM output) whose only sanitization was an implicit
third-party default: mermaid 11.x's securityLevel defaulting to
'strict'. No app-level backstop, no CSP — a future 'loose' tweak or
a mermaid sanitizer bypass (they have securityLevel-bypass CVEs in
their history) would have been live stored XSS.

Three coupled changes in notes.js:
- Pin securityLevel: 'strict' explicitly so it can't silently regress.
- htmlLabels: false (+ flowchart.htmlLabels) — labels render as native
  SVG <text> instead of foreignObject+XHTML. Verified empirically
  against the repo's exact dists (mermaid 11.15.0 / dompurify 3.4.8,
  headless Firefox): with HTML labels on, the SVG sanitize profile
  destroys every flowchart label, and re-allowing foreignObject still
  loses its HTML children to DOMPurify's namespace rules. Pure-SVG
  output sanitizes losslessly (flowchart 124->124, sequence 64->64,
  adversarial 81->81 elements; payloads survive only as inert escaped
  label text; DOM scan shows no on* attrs / script / javascript: URLs).
- App-level DOMPurify pass (SVG profiles) on mermaid's output before
  the v-html assignment, mirroring the markdown path's
  belt-and-suspenders.

code_review 1 round: no blocking issues, ship as-is. Pre-existing
non-blockers left untouched: the error-fallback <pre> construction
and the Date.now() render-id (collision-prone only sub-millisecond).
CSP for the admin app remains the ticket's deferred item 3.

Frontend-only; no migration. vite build:admin passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeffdafoe jeffdafoe merged commit 42db411 into main Jun 11, 2026
4 checks passed
@jeffdafoe jeffdafoe deleted the zbbs-work-395-admin-mermaid-xss-hardening branch June 11, 2026 16:17
jeffdafoe added a commit that referenced this pull request Jun 16, 2026
…234)

The notes viewer's mermaid path was the one v-html sink fed from
semi-trusted data (notes are authored by agents and the dream
pipeline's LLM output) whose only sanitization was an implicit
third-party default: mermaid 11.x's securityLevel defaulting to
'strict'. No app-level backstop, no CSP — a future 'loose' tweak or
a mermaid sanitizer bypass (they have securityLevel-bypass CVEs in
their history) would have been live stored XSS.

Three coupled changes in notes.js:
- Pin securityLevel: 'strict' explicitly so it can't silently regress.
- htmlLabels: false (+ flowchart.htmlLabels) — labels render as native
  SVG <text> instead of foreignObject+XHTML. Verified empirically
  against the repo's exact dists (mermaid 11.15.0 / dompurify 3.4.8,
  headless Firefox): with HTML labels on, the SVG sanitize profile
  destroys every flowchart label, and re-allowing foreignObject still
  loses its HTML children to DOMPurify's namespace rules. Pure-SVG
  output sanitizes losslessly (flowchart 124->124, sequence 64->64,
  adversarial 81->81 elements; payloads survive only as inert escaped
  label text; DOM scan shows no on* attrs / script / javascript: URLs).
- App-level DOMPurify pass (SVG profiles) on mermaid's output before
  the v-html assignment, mirroring the markdown path's
  belt-and-suspenders.

code_review 1 round: no blocking issues, ship as-is. Pre-existing
non-blockers left untouched: the error-fallback <pre> construction
and the Date.now() render-id (collision-prone only sub-millisecond).
CSP for the admin app remains the ticket's deferred item 3.

Frontend-only; no migration. vite build:admin passes.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant