ADR-0042: Mermaid can't draw in the page under the policy, so it is parked¶
Status: Accepted Date: 2026-10-17 Deciders: Alex Nodeland
Context¶
The chat's Markdown was to draw Mermaid's diagrams as inline SVG, with Mermaid's styles moved into constructable stylesheets, under the UIs' Content-Security-Policy as it is: style-src 'self', with no 'unsafe-inline', no hash and no nonce (ADR-0011). The policy was not to be loosened; if a spike showed the approach couldn't be done cleanly, it was to be reported rather than built.
The spike built a page as app and admin are built, served it with the router's policy(), and drove it in Chromium through Playwright, recording every securitypolicyviolation event. It used Mermaid 12.2.0 and DOMPurify 3.4.17, with securityLevel: "strict", theme: "base" and suppressErrorRendering: true, and drew eight kinds of diagram and two hostile ones.
What the spike found¶
-
mermaid.render()breaks the policy as it lays a diagram out, before it returns anything. It measures text in the live document: it inserts a<style>of its own, which is refused asstyle-src-elem, and setsstyleattributes through d3's.attr("style"), from 208 places in its code, which are refused asstyle-src-attr. Each kind of diagram broke it:Diagram style-src-elemstyle-src-attrflowchart 2 44 sequence 2 12 class 2 57 state 2 29 ER 2 16 gantt 2 13 pie 2 6 mindmap 3 28
- So both ways of showing what it draws fail. An image (
<img src="data:image/svg+xml,…">, whichimg-srcallows) and inline SVG with its styles moved into stylesheets each start from whatrender()returns, andrender()has already broken the policy by then. - Chromium reports every
styleattribute that any HTML parse in the page meets, whether throughDOMParser,createHTMLDocumentor a<template>, even in a document that never renders. Only writing styles through the CSS object model (element.style, a constructable stylesheet) is silent. Reworking Mermaid's SVG as a string afterwards can never reach zero, and DOMPurify, insiderender(), parses the SVG again. - Styles applied afterwards draw the wrong shapes. Mermaid sizes a diagram by its text as it is styled while it measures. With its styles held back until after
render(), edge labels were clipped ("ye" for "yes") and node labels sat at the top of their boxes. - Zero violations takes patching the DOM itself. For the length of each render, which is asynchronous, five of the browser's own members are replaced:
Element.prototype.setAttributeandsetAttributeNS,Node.prototype.insertBeforeandappendChild, and theinnerHTMLgetter. Each style becomes a rule in one constructable stylesheet, keyed by adata-attribute, and that sheet is adopted by the whole document while Mermaid measures. With that, all eight kinds and both hostile diagrams drew correctly with no style violation.- What it costs: every other library on the page runs against the replaced members while a render is in flight; two renders at once corrupt each other's restore, so it needs a lock; and it depends on how Mermaid 12.2.0 works inside, through its
setAttribute("style")calls and its serializing through the enclosing element'sinnerHTML. - What it still misses: a hostile label (
<img src=x onerror=…>) still raises onescript-src-attrreport, from DOMPurify's own parse, though nothing runs. A label's<img src="../x">survivesstrictand requests a URL of this origin while Mermaid measures.htmlLabels: falsestops that request, and shows<b>as text.
- What it costs: every other library on the page runs against the replaced members while a render is in flight; two renders at once corrupt each other's restore, so it needs a lock; and it depends on how Mermaid 12.2.0 works inside, through its
securityLevel: "sandbox"doesn't help. It returns<iframe src="data:text/html…">, which the policy refuses (frames fall back todefault-src 'self'), and it breaks the policy while drawing too.- It is large. A first flowchart loads 30 chunks, 649 kB compressed, the largest 447 kB; all of Mermaid's chunks are 1,453 kB.
Decision¶
- Mermaid is parked. A
mermaidfence shows as its source, inui's code block, with one quiet line, through the add-ons seam that ADR-0043 adds, as an add-on that is switched off does. Nothing draws it. - No patch of the DOM is built, and the policy is not loosened.
- The spike's code is not kept. This record holds what it found. Its page, server and driver are three short files, and the tests ADR-0043 adds hold the same policy, so a later attempt starts from those tests rather than from the spike.
Options considered¶
| Option | For | Against |
|---|---|---|
| A. Patch the DOM for each render, guarded | Diagrams now; zero style violations in the spike | A patch of the browser's own members, racing with other renders and tied to Mermaid's internals; a hostile label is still reported |
| B. Ask Mermaid for a rendering mode that writes styles through the CSS object model, and wait | Clean, for everyone who runs a strict policy | Not ours to schedule |
| C. Park it: show the source, with a quiet line (chosen) | Nothing to keep in step; Markdown's basics, highlighting and math don't wait for it | No diagrams |
D. A frame of its own with style-src 'unsafe-inline' |
Mermaid as it is | Loosens the policy, on one route; ruled out |
Consequences¶
- Easier: the Markdown's other add-ons go ahead without Mermaid, and none of them needs the policy to change.
- Harder: a model's diagram shows as text. The quiet line says why, and Copy takes the source to a viewer elsewhere.
- To revisit: when Mermaid can render through the CSS object model (B), or the owner accepts A with its guards. Either way, it must pass ADR-0043's tests under the policy, with no violation, before it ships.