Mermaid does not properly sanitize architecture diagram iconText leading to XSS
Description
Mermaid is a JavaScript based diagramming and charting tool that uses Markdown-inspired text definitions and a renderer to create and modify complex diagrams. In the default configuration of mermaid 11.9.0 and earlier, user supplied input for architecture diagram icons is passed to the d3 html() method, creating a sink for cross site scripting. This vulnerability is fixed in 11.10.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Mermaid ≤11.9.0 has XSS via architecture diagram icons because user input is passed to d3's html() method; fixed in 11.10.0.
Vulnerability
Summary Mermaid is a JavaScript-based diagramming tool that uses Markdown-like text to generate diagrams. In versions prior to 11.10.0, the architecture diagram component improperly sanitizes user-supplied icon labels. These labels are passed directly to the D3 html() method, which interprets the input as HTML without escaping, creating a stored or reflected cross-site scripting (XSS) sink [1][2].
Exploitation
Scenario An attacker can inject malicious HTML or JavaScript into an architecture diagram's icon label. For example, an icon value like `` would execute when the diagram is rendered. The attack requires only that a user views a crafted diagram, which could be embedded in a website, documentation, or shared via the Mermaid Live Editor. No authentication is needed if the attacker can supply the diagram content [3].
Impact
Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of the victim's browser, potentially leading to session theft, data exfiltration, or defacement. Because Mermaid is widely integrated (e.g., GitHub, GitLab, various documentation platforms), the vulnerability could affect many downstream users.
Mitigation
The issue is fixed in Mermaid version 11.10.0, released on August 19, 2025. Users should upgrade to this version or later. There is no known workaround for earlier versions. The fix includes sanitization of icon labels and icon SVGs before rendering [3].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mermaidnpm | >= 11.1.0, < 11.10.0 | 11.10.0 |
Affected products
2- Range: <=11.9.0
- mermaid-js/mermaidv5Range: >= 11.1.0, < 11.10.0
Patches
22aa833027951Merge commit from fork
8 files changed · +36 −13
.changeset/good-weeks-tickle.md+7 −0 added@@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix: sanitize icon labels and icon SVGs + +Resolves CVE-2025-54880 reported by @fourcube
cypress/integration/other/xss.spec.js+11 −0 modified@@ -142,6 +142,17 @@ describe('XSS', () => { cy.get('#the-malware').should('not.exist'); }); + it('should sanitize icon labels in architecture diagrams', () => { + const str = JSON.stringify({ + code: `architecture-beta + group api(cloud)[API] + service db "<img src=x onerror=\\"xssAttack()\\">" [Database] in api`, + }); + imgSnapshotTest(utf8ToB64(str), {}, true); + cy.wait(1000); + cy.get('#the-malware').should('not.exist'); + }); + it('should sanitize katex blocks', () => { const str = JSON.stringify({ code: `sequenceDiagram
cypress/integration/rendering/flowchart-v2.spec.js+3 −3 modified@@ -1118,7 +1118,7 @@ end imgSnapshotTest( `flowchart TB A(["Start"]) --> n1["Untitled Node"] - A --> n2["Untitled Node"] + A --> n2["Untitled Node"] `, {} ); @@ -1127,7 +1127,7 @@ end imgSnapshotTest( `flowchart BT n2["Untitled Node"] --> n1["Diamond"] - n1@{ shape: diam} + n1@{ shape: diam} `, {} ); @@ -1138,7 +1138,7 @@ end n2["Untitled Node"] --> n1["Rounded Rectangle"] n3["Untitled Node"] --> n1 n1@{ shape: rounded} - n3@{ shape: rect} + n3@{ shape: rect} `, {} );
demos/sankey.html+3 −3 modified@@ -20,14 +20,14 @@ <h2>FY20-21 Performance</h2> width: 800 nodeAlignment: left --- - sankey + sankey a,b,8 b,c,8 c,d,8 d,e,8 - + x,c,4 - c,y,4 + c,y,4 </pre> <h2>Energy flow</h2>
packages/mermaid/src/diagrams/architecture/svgDraw.ts+4 −2 modified@@ -3,6 +3,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; import { createText } from '../../rendering-util/createText.js'; import { getIconSVG } from '../../rendering-util/icons.js'; import type { D3Element } from '../../types.js'; +import { sanitizeText } from '../common/common.js'; import type { ArchitectureDB } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import { @@ -271,6 +272,7 @@ export const drawServices = async function ( elem: D3Element, services: ArchitectureService[] ): Promise<number> { + const config = getConfig(); for (const service of services) { const serviceElem = elem.append('g'); const iconSize = db.getConfigField('iconSize'); @@ -285,7 +287,7 @@ export const drawServices = async function ( width: iconSize * 1.5, classes: 'architecture-service-label', }, - getConfig() + config ); textElem @@ -320,7 +322,7 @@ export const drawServices = async function ( .attr('class', 'node-icon-text') .attr('style', `height: ${iconSize}px;`) .append('div') - .html(service.iconText); + .html(sanitizeText(service.iconText, config)); const fontSize = parseInt( window
packages/mermaid/src/docs/syntax/pie.md+1 −1 modified@@ -26,7 +26,7 @@ Drawing a pie chart is really simple in mermaid. **Note:** -> Pie chart values must be **positive numbers greater than zero**. +> Pie chart values must be **positive numbers greater than zero**. > **Negative values are not allowed** and will result in an error. [pie] [showData] (OPTIONAL)
packages/mermaid/src/rendering-util/createText.spec.ts+3 −2 modified@@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { replaceIconSubstring } from './createText.js'; +import { sanitizeText } from '../diagram-api/diagramAPI.js'; import mermaid from '../mermaid.js'; +import { replaceIconSubstring } from './createText.js'; describe('replaceIconSubstring', () => { it('converts FontAwesome icon notations to HTML tags', async () => { @@ -56,7 +57,7 @@ describe('replaceIconSubstring', () => { ]); const input = 'Icons galore: fa:fa-bell'; const output = await replaceIconSubstring(input); - const expected = staticBellIconPack.icons.bell.body; + const expected = sanitizeText(staticBellIconPack.icons.bell.body); expect(output).toContain(expected); }); });
packages/mermaid/src/rendering-util/icons.ts+4 −2 modified@@ -1,7 +1,9 @@ -import { log } from '../logger.js'; import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/types'; import type { IconifyIconCustomisations } from '@iconify/utils'; import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } from '@iconify/utils'; +import { getConfig } from '../config.js'; +import { sanitizeText } from '../diagrams/common/common.js'; +import { log } from '../logger.js'; interface AsyncIconLoader { name: string; @@ -100,5 +102,5 @@ export const getIconSVG = async ( ...renderData.attributes, ...extraAttributes, }); - return svg; + return sanitizeText(svg, getConfig()); };
734bde38777cfeat(arch): implemented node labels
8 files changed · +64 −11
packages/mermaid/src/diagrams/architecture/architectureDb.ts+2 −1 modified@@ -48,7 +48,7 @@ const clear = (): void => { commonClear(); }; -const addService = function ({ id, icon, in: parent, title }: Omit<ArchitectureService, "edges">) { +const addService = function ({ id, icon, in: parent, title, iconText }: Omit<ArchitectureService, "edges">) { if (state.records.registeredIds[id] !== undefined) { throw new Error(`The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`); } @@ -71,6 +71,7 @@ const addService = function ({ id, icon, in: parent, title }: Omit<ArchitectureS state.records.services[id] = { id, icon, + iconText, title, edges: [], in: parent,
packages/mermaid/src/diagrams/architecture/architectureStyles.ts+14 −0 modified@@ -19,6 +19,20 @@ const getStyles: DiagramStylesProvider = (options: ArchitectureStyleOptions) => stroke-width: ${options.archGroupBorderStrokeWidth}; stroke-dasharray: 8; } + .node-icon-text { + display: flex; + align-items: center; + } + + .node-icon-text > div { + color: #fff; + margin: 1px; + height: fit-content; + text-align: center; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + } `; export default getStyles;
packages/mermaid/src/diagrams/architecture/architectureTypes.ts+1 −0 modified@@ -183,6 +183,7 @@ export interface ArchitectureService { id: string; edges: ArchitectureEdge[]; icon?: string; + iconText?: string; title?: string; in?: string; width?: number;
packages/mermaid/src/diagrams/architecture/svgDraw.ts+30 −9 modified@@ -118,15 +118,16 @@ export const drawEdges = function (edgesEl: D3Element, cy: cytoscape.Core) { // Calculate the new width/height with the rotation applied, and transform to the proper position const bboxNew = textElem.node().getBoundingClientRect(); - textElem - .attr('transform', ` - translate(${midX}, ${midY - (bboxOrig.height / 2)}) - translate(${x * bboxNew.width / 2}, ${y * bboxNew.height / 2}) + textElem.attr( + 'transform', + ` + translate(${midX}, ${midY - bboxOrig.height / 2}) + translate(${(x * bboxNew.width) / 2}, ${(y * bboxNew.height) / 2}) rotate(${-1 * x * y * 45}, 0, ${bboxOrig.height / 2}) - `); + ` + ); } } - } } }); @@ -163,12 +164,16 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { getIcon(data.icon)?.(bkgElem, groupIconSize); bkgElem.attr( 'transform', - 'translate(' + (shiftedX1 + halfIconSize + 1) + ', ' + (shiftedY1 + halfIconSize + 1) + ')' + 'translate(' + + (shiftedX1 + halfIconSize + 1) + + ', ' + + (shiftedY1 + halfIconSize + 1) + + ')' ); shiftedX1 += groupIconSize; // TODO: test with more values // - 1 - 2 comes from the Y axis transform of the icon and label - shiftedY1 += ((fontSize / 2) - 1 - 2); + shiftedY1 += fontSize / 2 - 1 - 2; } if (data.label) { const textElem = groupLabelContainer.append('g'); @@ -190,7 +195,11 @@ export const drawGroups = function (groupsEl: D3Element, cy: cytoscape.Core) { textElem.attr( 'transform', - 'translate(' + (shiftedX1 + halfIconSize + 4) + ', ' + (shiftedY1 + halfIconSize + 2) + ')' + 'translate(' + + (shiftedX1 + halfIconSize + 4) + + ', ' + + (shiftedY1 + halfIconSize + 2) + + ')' ); } } @@ -218,6 +227,7 @@ export const drawServices = function ( }, getConfig() ); + textElem .attr('dy', '1em') .attr('alignment-baseline', 'middle') @@ -234,6 +244,17 @@ export const drawServices = function ( // throw new Error(`Invalid SVG Icon name: "${service.icon}"`); // } bkgElem = getIcon(service.icon)?.(bkgElem, iconSize); + } else if (service.iconText) { + bkgElem = getIcon('blank')?.(bkgElem, iconSize); + const textElemContainer = bkgElem.append('g'); + const fo = textElemContainer.append('foreignObject').attr('width', iconSize).attr('height', iconSize); + const divElem = fo + .append('div') + .attr('class', 'node-icon-text') + .attr('style', `height: ${iconSize}px;`) + .append('div').html(service.iconText); + const fontSize = parseInt(window.getComputedStyle(divElem.node(), null).getPropertyValue("font-size").replace(/[^\d]/g, '')) ?? 16; + divElem.attr('style', `-webkit-line-clamp: ${Math.floor((iconSize - 2) / fontSize)};`) } else { bkgElem .append('path')
packages/mermaid/src/rendering-util/svg/blank.ts+11 −0 added@@ -0,0 +1,11 @@ +/** + * Designer: Nicolas Newman + */ +import { createIcon } from '../svgRegister.js'; + +export default createIcon( + `<g> + <rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/> +</g>`, + 80 +);
packages/mermaid/src/rendering-util/svg/index.ts+2 −0 modified@@ -5,6 +5,7 @@ import disk from './disk.js'; import internet from './internet.js'; import cloud from './cloud.js'; import unknown from './unknown.js'; +import blank from './blank.js'; const defaultIconLibrary: IconLibrary = { database: database, @@ -13,6 +14,7 @@ const defaultIconLibrary: IconLibrary = { internet: internet, cloud: cloud, unknown: unknown, + blank: blank, }; export default defaultIconLibrary;
packages/parser/src/language/architecture/architecture.langium+2 −1 modified@@ -26,7 +26,7 @@ Group: ; Service: - 'service' id=ARCH_ID icon=ARCH_ICON? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL + 'service' id=ARCH_ID (iconText=ARCH_TEXT_ICON | icon=ARCH_ICON)? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL ; Edge: @@ -35,6 +35,7 @@ Edge: terminal ARROW_DIRECTION: 'L' | 'R' | 'T' | 'B'; terminal ARCH_ID: /[\w]+/; +terminal ARCH_TEXT_ICON: /\("[^"]+"\)/; terminal ARCH_ICON: /\([\w]+\)/; terminal ARCH_TITLE: /\[[\w ]+\]/; terminal ARROW_INTO: /\(|\)/; \ No newline at end of file
packages/parser/src/language/architecture/valueConverter.ts+2 −0 modified@@ -11,6 +11,8 @@ export class ArchitectureValueConverter extends AbstractMermaidValueConverter { ): ValueType | undefined { if (rule.name === 'ARCH_ICON') { return input.replace(/[()]/g, '').trim(); + } else if (rule.name === 'ARCH_TEXT_ICON') { + return input.replace(/[()"]/g, ''); } else if (rule.name === 'ARCH_TITLE') { return input.replace(/[[\]]/g, '').trim(); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-8gwm-58g9-j8pwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54880ghsaADVISORY
- github.com/mermaid-js/mermaid/commit/2aa83302795183ea5c65caec3da1edd6cb4791fcghsax_refsource_MISCWEB
- github.com/mermaid-js/mermaid/commit/734bde38777c9190a5a72e96421c83424442d4e4ghsax_refsource_MISCWEB
- github.com/mermaid-js/mermaid/security/advisories/GHSA-8gwm-58g9-j8pwghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.