Skip to content

fix(mermaid): inject SVG via HTML parser, not strict XML#54

Open
thomnico wants to merge 1 commit into
nicobailon:mainfrom
thomnico:fix/mermaid-injection-html-parse
Open

fix(mermaid): inject SVG via HTML parser, not strict XML#54
thomnico wants to merge 1 commit into
nicobailon:mainfrom
thomnico:fix/mermaid-injection-html-parse

Conversation

@thomnico
Copy link
Copy Markdown

@thomnico thomnico commented May 1, 2026

fix(mermaid): inject SVG via HTML parser, not strict XML

TL;DR

Switch templates/mermaid-flowchart.html from setting the canvas's HTML
property to a DOMParser that parses the SVG as text/html (HTML5,
lenient) instead of image/svg+xml (strict XML). Document the trap in
references/css-patterns.md.

Why

Mermaid 10+ emits HTML inside <foreignObject> for multi-line node labels:

<foreignObject>
  <p>line one<br>line two</p>
</foreignObject>

The unclosed <br> is valid HTML but not valid XML. That distinction
silently breaks two patterns the skill — and agents reusing the skill under
stricter security policies — naturally reach for:

Approach XSS scanner Renders Mermaid Verdict
Setting the host's HTML property to the SVG string (canonical) Flagged as a potential sink Yes Fine without a scanner; agents under Semgrep refuse to emit it
parseFromString(svg, 'image/svg+xml') + adoptNode Passes No — parser stops at the first <br>, silently truncates the SVG Looks like a "one-node diagram" bug
parseFromString(svg, 'text/html') + adoptNode Passes Yes — HTML5 parser handles <br> and preserves the SVG namespace via foreign-content rules Use this

The XML-parser failure mode is the dangerous one because it does not
throw — only the first node renders, every edge disappears, and the
browser console is empty. The underlying error surfaces only via Chrome's
page error overlay: Opening and ending tag mismatch: br line 1 and p.

Repro (before fix)

A real-world plan-review diagram I generated under a Semgrep policy that
blocks the canonical injection pattern:

  • Source: 15 nodes, 17 edges, multi-line labels
  • Rendered before fix: 1 node, 0 edges
  • Rendered after fix: 15 nodes, 17 edges

Changes

  • plugins/visual-explainer/templates/mermaid-flowchart.html
    • Replace the HTML-property assignment in render() with DOMParser
      • text/html + adoptNode
    • Inline comment pointing to the docs
  • plugins/visual-explainer/references/css-patterns.md
    • Update the inline render() example to use the same pattern
    • Add a new subsection "Injecting Mermaid SVG: pick the right parser"
      with the comparison table, failure mode, and a canonical
      injectSvg(host, svgMarkup) helper

Compatibility

  • Works in all browsers that support DOMParser with text/html parsing —
    i.e. every browser that supports Mermaid 10+ in the first place.
  • No behaviour change in browsers where the previous pattern worked: the
    parsed SVG is identical.
  • Removes a CSP / XSS-scanner blocker that prevents the skill from being
    usable in security-hardened environments.

Out of scope

  • The openInNewTab handler still serialises via a template literal;
    that string is then parsed by the browser when the new tab loads, so it
    does not hit the same trap. Left as-is to keep the diff minimal.

Mermaid 10+ embeds HTML inside <foreignObject> for multi-line node labels
(e.g. <p>line one<br>line two</p>). The unclosed <br> is valid HTML but
not valid XML, which silently breaks two seemingly-safe injection paths:

  - Element.innerHTML = svg
      Works in browsers, but flagged as a potential XSS sink by
      Semgrep / CSP-aware linters in many user environments. Agents
      operating under those scanners reach for DOMParser as a workaround.

  - DOMParser.parseFromString(svg, 'image/svg+xml') + adoptNode
      Passes XSS scanners but the strict XML parser stops parsing at the
      first <br> without throwing. The result is a truncated SVG: only
      the first node renders, every edge disappears, no error in the
      console of most browsers (Chrome surfaces it only as
      "Opening and ending tag mismatch: br line 1 and p" via the page
      error overlay, not the dev console).

Switch the canonical render() in templates/mermaid-flowchart.html to
DOMParser.parseFromString(svg, 'text/html') + adoptNode. The HTML5
parser is lenient about <br> and preserves the SVG namespace via
foreign-content rules, giving us a path that:

  1. Passes XSS scanners (no innerHTML write).
  2. Renders complex Mermaid diagrams correctly.
  3. Surfaces real errors instead of silently truncating.

Document the trade-offs and the failure mode in css-patterns.md so
future skill maintainers (and agents that re-implement render() under
stricter security policies) don't fall back into the XML-parser trap.

Verified with a real-world plan-review diagram (15 nodes, 17 edges,
multi-line labels): rendered before fix = 1 node / 0 edges; after
fix = 15 nodes / 17 edges.
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