Build1 distinct publisher3 min readPublished
A green CodeQL run and a two-line sanitiser call would have left the boxes and arrows intact on four of five Mermaid diagram types while quietly deleting the text inside them. Counting elements is what caught it.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
foreignObject is the hatch where an SVG document hands off to HTML. Mermaid draws a text label by opening one and putting a div inside it, with a p and a span inside that [15]. The obvious fix asks DOMPurify for two allow lists, svg and svgFilters [12]. Neither has an entry for div. DOMPurify drops a tag it does not recognise along with that tag's subtree, so the whole label goes, and the sanitiser is behaving exactly as configured.
Add up the four affected rows of the author's element count [14]. Eleven foreignObject nodes, eleven spans, eleven p nodes, and eleven of the fifteen divs: 44 elements removed across four diagrams [1]. Every one of them is text or a container for text. Boxes, arrows and layout are pure SVG, so they survive [16], and the diagram still draws at the right size in the right place with nothing written in it. There is no exception to catch and no assertion to fail.
The instructive part is that Forem's own HTML scrubber has the same hole. Its allow list is missing foreignObject, along with text, tspan, marker and style, which is why rendering the SVG on the server was never viable even though svg, path and g are permitted [5]. Two allow lists, written by different people against different threat models, both treat the label carrier as unrecognised markup. If your sanitiser and your scrubber were both written against the SVG spec, and your renderer draws text through the HTML escape hatch, this is the default outcome rather than a bug.
That is also the condition for the numbers transferring. Sequence diagrams lost nothing [14], so those labels are not going through foreignObject at all [2]. Run Mermaid with HTML labels off and the svg profile is close to transparent, and the CodeQL one-liner is fine as written. Run the default and four of the five types tested here degrade [16], which means a spot check on one randomly chosen diagram would have surfaced the loss four times in five [3].
The check that caught it is cheap: render a handful of diagrams, sanitize the output, count elements on each side, compare [13]. CodeQL closes either way [12]. And the alert was correct about the shape of the problem, because user-authored diagram source really does reach innerHTML by way of mermaid.render() [9]. The author's reason for not simply leaning on Mermaid's strict mode, which already sanitizes its own output with DOMPurify [10], is that trusting a dependency's internal sanitisation for untrusted input is the pattern behind Mermaid's own historical XSS advisories [11]. I agree with the direction, and the one-liner is still the right shape of fix. It needed the element count sitting next to it before the merge.
Ranked by verification strength, evidence, and original report placement.
The author shipped Mermaid diagram support to Forem, the open source platform that powers DEV, so that fenced mermaid code blocks render as diagrams instead of highlighted source.
Forem renders Markdown through Redcarpet with a Rouge highlighter, and because Rouge already receives the language hint, intercepting block_code is enough to emit different markup for Mermaid.
The markup used in Mermaid's own documentation, <pre class="mermaid">, fails silently on Forem: every piece of rendered Markdown passes through a scrubber with an allow list, class is not on it, and the attribute is stripped before the HTML reaches the browser with no error raised.
data-lang is on Forem's allow list, so the markup became <pre data-lang="mermaid"> and the client script targets pre[data-lang="mermaid"].
Server-side SVG rendering looked viable because Forem's allowed tag list includes svg, path and g, but the list is missing text, tspan, foreignObject, marker and style, which is all the labels, all the arrowheads and all the styling, so server-rendered diagrams would have arrived mangled.
Forem post-processes rendered HTML, converting :smile: into an emoji and @name into a profile link, both of which are destructive for diagram source where a sequence diagram label may legitimately contain either.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 1, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
A success message keyed on reaching a line reported 39 days of pushes that never left the box1 distinct publisher
build
Your scanner finds it in seconds; the average fix now takes 252 days1 distinct publisher
build
Ten planted bugs, about a dollar of API spend, and the case for grading the log not the answer1 distinct publisher
build
AI-written code fails the same four ways, and every gate you own reports green1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-hand, checkable, unreplicated
The load of this story is carried by pasted terminal output: five diagram types, one line each, element counts before and after sanitising. That is unusually easy for a reader to rerun — install Mermaid and DOMPurify and you have your own numbers in minutes — and the mechanism it exposes is consistent with why foreignObject is treated as a namespace-confusion vector in the first place. What is missing is anyone else: no pull request link, no reviewer, no Forem maintainer confirming the markup that shipped, and one publisher on the whole story.
One platform, self-reported
Concrete but narrow: the feature is live enough that a flowchart renders inside the post on DEV, which is more than a proposal and less than traction. Beyond that single deployment there is nothing to count — no other platform using the data-lang approach, no indication anyone else has moved htmlLabels to the top level, and the pull request's fate is described by its author alone.
Sold more modestly than it deserves
The headline number is the measurement, not a stretch of it: four of five types lose their labels because four of five rows lose every foreignObject. The author also passes on the easy dramatic claim, saying outright he does not think the code was exploitable. If anything the more portable lesson is buried past the midpoint — the Mermaid options he believed were hardening the renderer were nested one level too deep and doing nothing, and only counting elements revealed it.
His patch, his platform, his write-up
A contributor describing his own contribution, published on the site that runs the code — DEV carrying a post about Forem — is a setup that usually bends toward flattery. This one bends the other way: the misconfigured Mermaid options were his, the near-miss was his, and the reputational payoff comes from the catch rather than the feature. The residual distortion is about scope, not spin. Nobody adversarial has looked at the merged code, and the choice to route around Forem's allow list instead of arguing for widening it goes unexamined.
Act on it, don't cite it
Split by subject. The sanitizer behaviour is safe to rely on — DOMPurify removing foreignObject regardless of profile is reproducible in a minute and coherent with the threat it defends against — so a builder can apply the top-level htmlLabels lesson today. The Forem particulars, the CodeQL run going green, and the state of the pull request are all single-sourced from the person who wrote them, so they belong in a working note rather than a reference.