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

Mermaid: Improper sanitization of configuration leads to CSS injection

CVE-2026-41159

Description

Impact

Mermaid's default configuration allows injecting CSS that applies outside of the Mermaid diagram via the fontFamily, themeCSS, and altFontFamily configuration options.

Live demo: mermaid.live

Example code:

%%{init: {"fontFamily": "x;a{b} :not(&){background:green !important} c{d}"}}%%
flowchart LR
    A --> B

The injected CSS exploits stylis's & (scope reference) handling. :not(&) escapes the #mermaid-xxx automatic scoping, applying styles to all page elements. Global at-rules (@font-face, @keyframes, @counter-style) are also injectable as stylis hoists them to top level.

This allows page defacement and DOM attribute exfiltration via CSS :has() selectors.

Patches

Workarounds

If you can't upgrade mermaid, you can set the `secure` config value in the mermaid config to avoid allowing diagrams to modify fontFamily, themeCSS, altFontFamily, and themeVariables.

Setting `"securityLevel": "sandbox"` will also prevent this.

Credits

Reported by @zsxsoft on behalf of @KeenSecurityLab

AI Insight

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

Mermaid's default configuration allows CSS injection via fontFamily/themeCSS, enabling page defacement and attribute exfiltration.

Vulnerability

Mermaid's default configuration allows injecting arbitrary CSS that escapes the SVG's scoped namespace via the fontFamily, themeCSS, and altFontFamily options. The injected CSS leverages stylis's handling of the & selector with :not(&) to break out of the automatic #mermaid-xxx scoping, applying styles to the entire page. Global at-rules like @font-face can also be injected as stylis hoists them.[1]

Exploitation

An attacker with the ability to provide Mermaid diagram configuration (e.g., via a live editor or a user-supplied diagram in an application) can inject a payload. For example, setting fontFamily to x;a{b} :not(&){background:green !important} c{d} causes the CSS to apply to all page elements. No authentication beyond control of the diagram input is required, and the attack can be performed without user interaction if the diagram is rendered automatically.[1]

Impact

Successful exploitation allows page defacement (e.g., changing background colors) and, more critically, DOM attribute exfiltration via CSS :has() selectors. This can leak sensitive information such as CSRF tokens or other data stored in HTML attributes, leading to further compromise.[1]

Mitigation

Patches are available in Mermaid v11.15.0 and v10.9.6, which introduce proper CSS namespace escaping using stylis's middleware to ensure all rules are prefixed with the SVG element's ID.[2][3][4] For users unable to upgrade, workarounds include setting the secure config value to block the vulnerable options or enabling "securityLevel": "sandbox" to isolate diagram rendering.[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

1

Patches

2
64769738d5b5

fix: prevent CSS namespace escape using `:not(&)`

https://github.com/mermaid-js/mermaidAlois KlinkApr 1, 2026via ghsa
4 files changed · +80 6
  • .changeset/social-masks-visit.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'mermaid': patch
    +---
    +
    +fix: handle `&` when namespacing CSS rules
    
  • cypress/integration/other/ghsa.spec.js+14 0 modified
    @@ -41,4 +41,18 @@ describe('CSS injections', () => {
         );
         cy.get('body > div #pwned').should('not.exist');
       });
    +  it('should prevent CSS namespace injection via :not(&)', () => {
    +    imgSnapshotTest(
    +      `---
    +title: Green background CSS should not be able to escape the diagram using :not(&)
    +config:
    +  themeCSS: ':not(&){background:green !important}'
    +---
    +flowchart
    +  A --> B
    +     `,
    +      { logLevel: 1 }
    +    );
    +    cy.get('body').should('not.have.css', 'background-color', 'rgb(0, 128, 0)');
    +  });
     });
    
  • packages/mermaid/src/mermaidAPI.spec.ts+15 0 modified
    @@ -461,6 +461,21 @@ describe('mermaidAPI', () => {
             '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .default{color:red;}#someId .classDef2>*{color:purple;}#someId .classDef2 span{color:purple;}'
           );
         });
    +
    +    it('should handle `:not(&)` selectors in the CSS', () => {
    +      const result = createUserStyles(
    +        {
    +          ...mockConfig,
    +          themeCSS: ':not(&){background:green !important}',
    +        },
    +        'someDiagram',
    +        new Map(),
    +        '#someId'
    +      );
    +      expect(result).toEqual(
    +        '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId :not(#someId){background:green!important;}'
    +      );
    +    });
       });
     
       describe('removeExistingElements', () => {
    
  • packages/mermaid/src/mermaidAPI.ts+46 6 modified
    @@ -4,7 +4,7 @@
      */
     // @ts-ignore TODO: Investigate D3 issue
     import { select } from 'd3';
    -import { compile, serialize, stringify } from 'stylis';
    +import { compile, middleware, serialize, stringify } from 'stylis';
     import DOMPurify from 'dompurify';
     import isEmpty from 'lodash-es/isEmpty.js';
     import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
    @@ -188,6 +188,50 @@ export const createCssStyles = (
       return cssString + cssStyleSheetToString(cssStyles);
     };
     
    +/**
    + * Use `stylis` to compile the CSS to only apply to the given namespace.
    + *
    + * This will also remove some newer CSS features (e.g. nesting) to better
    + * support older browsers and does some minification.
    + *
    + * @internal
    + * @param namespace - the namespace to add in front of all the CSS styles, e.g. `#idOfSvgElement`
    + * @param css - the CSS styles to add the namespace to.
    + * @see https://github.com/thysultan/stylis
    + *
    + * @example
    + * // Returns `#id .class1{fill:red;}`
    + * compileCSS('#id', `.class1 { fill: red }`)
    + */
    +const compileCSS = (namespace: `#${string}`, css: string) => {
    +  return serialize(
    +    compile(`${namespace}{${css}}`),
    +    middleware([
    +      function addNamespace(element, _index, _children, _callback) {
    +        /**
    +         * CSS normally automatically adds the `&` selector in front of each
    +         * element. But, if there's already an `&` selector, it doesn't add this.
    +         *
    +         * This code will explicitly make sure it's always added, to ensure
    +         * that the CSS never applies outside the SVG.
    +         *
    +         * E.g. `#svgId { .nested-class :not(&) { fill: red } }` will be
    +         * transformed to `#svgId { & .nested-class :not(&) { fill: red } }`
    +         */
    +        if (element.type === 'rule' && Array.isArray(element.props)) {
    +          element.props = element.props.map((prop) => {
    +            if (!prop.startsWith(namespace)) {
    +              return `${namespace} ${prop}`;
    +            }
    +            return prop;
    +          });
    +        }
    +      },
    +      stringify,
    +    ])
    +  );
    +};
    +
     export const createUserStyles = (
       config: MermaidConfig,
       graphType: string,
    @@ -202,11 +246,7 @@ export const createUserStyles = (
         { ...config.themeVariables, theme: config.theme, look: config.look },
         svgId
       );
    -
    -  // Now turn all of the styles into a (compiled) string that starts with the id
    -  // use the stylis library to compile the css, turn the results into a valid CSS string (serialize(...., stringify))
    -  // @see https://github.com/thysultan/stylis
    -  return serialize(compile(`${svgId}{${allStyles}}`), stringify);
    +  return compileCSS(svgId, allStyles);
     };
     
     /**
    
a9d9f0d8eb79

fix: prevent CSS namespace escape using `:not(&)`

https://github.com/mermaid-js/mermaidAlois KlinkApr 1, 2026via ghsa
4 files changed · +83 13
  • cypress/integration/other/ghsa.spec.js+14 0 modified
    @@ -41,4 +41,18 @@ describe('CSS injections', () => {
         );
         cy.get('body > div #pwned').should('not.exist');
       });
    +  it('should prevent CSS namespace injection via :not(&)', () => {
    +    imgSnapshotTest(
    +      `---
    +title: Green background CSS should not be able to escape the diagram using :not(&)
    +config:
    +  themeCSS: ':not(&){background:green !important}'
    +---
    +flowchart
    +  A --> B
    +     `,
    +      { logLevel: 1 }
    +    );
    +    cy.get('body').should('not.have.css', 'background-color', 'rgb(0, 128, 0)');
    +  });
     });
    
  • docs/config/setup/modules/mermaidAPI.md+6 6 modified
    @@ -96,7 +96,7 @@ mermaid.initialize(config);
     
     #### Defined in
     
    -[mermaidAPI.ts:639](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L639)
    +[mermaidAPI.ts:679](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L679)
     
     ## Functions
     
    @@ -127,7 +127,7 @@ Return the last node appended
     
     #### Defined in
     
    -[mermaidAPI.ts:291](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L291)
    +[mermaidAPI.ts:331](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L331)
     
     ---
     
    @@ -153,7 +153,7 @@ the cleaned up svgCode
     
     #### Defined in
     
    -[mermaidAPI.ts:237](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L237)
    +[mermaidAPI.ts:277](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L277)
     
     ---
     
    @@ -201,7 +201,7 @@ the string with all the user styles
     
     #### Defined in
     
    -[mermaidAPI.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L213)
    +[mermaidAPI.ts:257](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L257)
     
     ---
     
    @@ -254,7 +254,7 @@ Put the svgCode into an iFrame. Return the iFrame code
     
     #### Defined in
     
    -[mermaidAPI.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L268)
    +[mermaidAPI.ts:308](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L308)
     
     ---
     
    @@ -279,4 +279,4 @@ Remove any existing elements from the given document
     
     #### Defined in
     
    -[mermaidAPI.ts:344](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L344)
    +[mermaidAPI.ts:384](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L384)
    
  • packages/mermaid/src/mermaidAPI.spec.ts+17 1 modified
    @@ -74,12 +74,13 @@ vi.mock('stylis', async (importOriginal) => {
       // eslint-disable-next-line @typescript-eslint/consistent-type-imports
       const original: typeof import('stylis') = await importOriginal();
       return {
    +    middleware: vi.fn().mockImplementation(original.middleware),
         stringify: vi.fn().mockImplementation(original.stringify),
         compile: vi.fn().mockImplementation(original.compile),
         serialize: vi.fn().mockImplementation(original.serialize),
       };
     });
    -import { compile, serialize } from 'stylis';
    +import { compile, middleware, serialize } from 'stylis';
     import { decodeEntities, encodeEntities } from './utils.js';
     import { Diagram } from './Diagram.js';
     
    @@ -517,6 +518,21 @@ describe('mermaidAPI', () => {
             '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .default{color:red;}#someId .classDef2>*{color:purple;}#someId .classDef2 span{color:purple;}'
           );
         });
    +
    +    it('should handle `:not(&)` selectors in the CSS', () => {
    +      const result = createUserStyles(
    +        {
    +          ...mockConfig,
    +          themeCSS: ':not(&){background:green !important}',
    +        },
    +        'someDiagram',
    +        {},
    +        '#someId'
    +      );
    +      expect(result).toEqual(
    +        '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId :not(#someId){background:green!important;}'
    +      );
    +    });
       });
     
       describe('removeExistingElements', () => {
    
  • packages/mermaid/src/mermaidAPI.ts+46 6 modified
    @@ -12,7 +12,7 @@
      */
     // @ts-ignore TODO: Investigate D3 issue
     import { select } from 'd3';
    -import { compile, serialize, stringify } from 'stylis';
    +import { compile, middleware, serialize, stringify } from 'stylis';
     // @ts-ignore: TODO Fix ts errors
     import { version } from '../package.json';
     import * as configApi from './config.js';
    @@ -210,6 +210,50 @@ export const createCssStyles = (
       return cssString + cssStyleSheetToString(cssStyles);
     };
     
    +/**
    + * Use `stylis` to compile the CSS to only apply to the given namespace.
    + *
    + * This will also remove some newer CSS features (e.g. nesting) to better
    + * support older browsers and does some minification.
    + *
    + * @internal
    + * @param namespace - the namespace to add in front of all the CSS styles, e.g. `#idOfSvgElement`
    + * @param css - the CSS styles to add the namespace to.
    + * @see https://github.com/thysultan/stylis
    + *
    + * @example
    + * // Returns `#id .class1{fill:red;}`
    + * compileCSS('#id', `.class1 { fill: red }`)
    + */
    +const compileCSS = (namespace: `#${string}`, css: string) => {
    +  return serialize(
    +    compile(`${namespace}{${css}}`),
    +    middleware([
    +      function addNamespace(element, _index, _children, _callback) {
    +        /**
    +         * CSS normally automatically adds the `&` selector in front of each
    +         * element. But, if there's already an `&` selector, it doesn't add this.
    +         *
    +         * This code will explicitly make sure it's always added, to ensure
    +         * that the CSS never applies outside the SVG.
    +         *
    +         * E.g. `#svgId { .nested-class :not(&) { fill: red } }` will be
    +         * transformed to `#svgId { & .nested-class :not(&) { fill: red } }`
    +         */
    +        if (element.type === 'rule' && Array.isArray(element.props)) {
    +          element.props = element.props.map((prop) => {
    +            if (!prop.startsWith(namespace)) {
    +              return `${namespace} ${prop}`;
    +            }
    +            return prop;
    +          });
    +        }
    +      },
    +      stringify,
    +    ])
    +  );
    +};
    +
     export const createUserStyles = (
       config: MermaidConfig,
       graphType: string,
    @@ -219,11 +263,7 @@ export const createUserStyles = (
     ): string => {
       const userCSSstyles = createCssStyles(config, classDefs);
       const allStyles = getStyles(graphType, userCSSstyles, config.themeVariables);
    -
    -  // Now turn all of the styles into a (compiled) string that starts with the id
    -  // use the stylis library to compile the css, turn the results into a valid CSS string (serialize(...., stringify))
    -  // @see https://github.com/thysultan/stylis
    -  return serialize(compile(`${svgId}{${allStyles}}`), stringify);
    +  return compileCSS(svgId, allStyles);
     };
     
     /**
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

6

News mentions

0

No linked articles in our index yet.