Skip to content

fix(frontend): set Mermaid securityLevel to strict to prevent XSS#898

Open
sebastiondev wants to merge 1 commit into
chaitin:mainfrom
sebastiondev:fix/cwe79-mermaid-strict
Open

fix(frontend): set Mermaid securityLevel to strict to prevent XSS#898
sebastiondev wants to merge 1 commit into
chaitin:mainfrom
sebastiondev:fix/cwe79-mermaid-strict

Conversation

@sebastiondev

Copy link
Copy Markdown

Summary

The Markdown renderer in frontend/src/components/common/markdown.tsx initialises Mermaid with securityLevel: "loose". In that mode Mermaid does not sanitize label/node text and allows raw HTML (including <script>, <img onerror=...>, javascript: links, click-bindings, etc.) to pass through into the rendered SVG. That SVG is then injected into the DOM via dangerouslySetInnerHTML, so any script content in a Mermaid diagram executes in the victim's browser origin.

The markdown pipeline uses rehypeSanitize for regular HTML, but Mermaid diagrams are handled by a separate custom code-block renderer that bypasses that sanitizer entirely. securityLevel: "loose" is therefore the only gate, and it is disabled.

  • CWE: CWE-79 (Stored XSS via Markdown/Mermaid)
  • File: frontend/src/components/common/markdown.tsx (Mermaid init, line ~44)
  • Severity: Medium–High in a multi-user AI assistant where markdown from one user (shared task, chat transcript, LLM output influenced by prompt injection, imported task) is rendered in another user's session.

Fix

One-line change: switch Mermaid's securityLevel from "loose" to "strict".

-          securityLevel: "loose",
+          securityLevel: "strict",

In "strict" mode, Mermaid HTML-escapes user text in labels and disables click/callback bindings, which is the vendor-recommended setting for rendering untrusted diagram source. This is the minimal, targeted mitigation and does not affect diagram layout, theming, or any legitimate Mermaid syntax — only inline HTML/JS injected into labels is neutralised.

Proof of Concept

With the pre-patch build, render any markdown containing the following fenced block (via a shared task description, chat message, or LLM response):

```mermaid
graph TD
  A["<img src=x onerror='alert(document.domain)'>"] --> B[ok]
```

Before the fix: on loose, Mermaid emits the <img> unescaped inside the SVG <foreignObject>, the onerror fires when the image fails to load, and alert(document.domain) executes in the MonkeyCode origin. Session cookies, task data, and any authenticated API accessible to the victim are then reachable to attacker-controlled JS.

After the fix: on strict, Mermaid HTML-escapes the label text; the <img> renders as literal text inside the diagram node and no script executes.

Mermaid's own security documentation describes the same class of issue: https://mermaid.js.org/config/usage.html#securitylevel

What we tested

  • Verified the Mermaid component is the only markdown path that bypasses rehypeSanitize — the rest of the pipeline routes HTML through rehypeSanitize.
  • Confirmed the SVG output is injected via dangerouslySetInnerHTML={{ __html: svg }} in the same file (line ~73), so anything Mermaid emits reaches the DOM as HTML.
  • Confirmed the fix is a config-only change: no API surface change, no impact on diagram rendering, no new dependencies. Existing valid Mermaid diagrams continue to render identically.

Adversarial review

Before submitting, we tried to disprove the finding. Two things could have made it a non-issue: (1) an upstream sanitizer between Mermaid's SVG and the DOM, or (2) Mermaid handling HTML safely even in loose mode. Neither holds — rehypeSanitize is applied only to the HAST tree, not to the raw SVG string produced by mermaid.render(), and Mermaid documents that loose explicitly permits HTML in labels. We also considered whether the markdown source is always trusted; in a shared-workspace AI product, task/chat/LLM output is routinely rendered cross-user, so the untrusted-input precondition is satisfied by normal product usage.


Discovered by the Sebastion AI GitHub App.

The Mermaid renderer's per-render initialize() call was overriding the
top-level 'strict' setting with 'loose'. With securityLevel: 'loose',
Mermaid does not sanitize scripting/interactive constructs in the
generated SVG, and the SVG is then injected into the DOM via
dangerouslySetInnerHTML — allowing XSS from any attacker-controllable
markdown source (chat messages, task descriptions, etc.).

Aligns the runtime re-initialization with the module-level default.
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