VYPR
Moderate severityNVD Advisory· Published Aug 19, 2025· Updated Aug 19, 2025

Mermaid does not properly sanitize architecture diagram iconText leading to XSS

CVE-2025-54880

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.

PackageAffected versionsPatched versions
mermaidnpm
>= 11.1.0, < 11.10.011.10.0

Affected products

2
  • Range: <=11.9.0
  • mermaid-js/mermaidv5
    Range: >= 11.1.0, < 11.10.0

Patches

2
2aa833027951

Merge commit from fork

https://github.com/mermaid-js/mermaidSidharth VinodAug 18, 2025via ghsa
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());
     };
    
734bde38777c

feat(arch): implemented node labels

https://github.com/mermaid-js/mermaidNicolasNewmanMay 6, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.