Mermaid: Improper sanitization of configuration leads to CSS injection
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
- v11.15.0 (see 64769738d5b59211e1decb471ffbaca8afec51aa)
- v10.9.6 (see a9d9f0d8eb790349121508688cd338253fd80d76)
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]
- GitHub - mermaid-js/mermaid: Generation of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown
- Release mermaid@11.15.0 · mermaid-js/mermaid
- fix: prevent CSS namespace escape using `:not(&)` · mermaid-js/mermaid@a9d9f0d
- fix: prevent CSS namespace escape using `:not(&)` · mermaid-js/mermaid@6476973
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.
| Package | Affected versions | Patched versions |
|---|---|---|
mermaidnpm | >= 11.0.0-alpha.1, < 11.15.0 | 11.15.0 |
mermaidnpm | < 10.9.6 | 10.9.6 |
Affected products
1- Range: <= 10.9.5
Patches
264769738d5b5fix: prevent CSS namespace escape using `:not(&)`
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); }; /**
a9d9f0d8eb79fix: prevent CSS namespace escape using `:not(&)`
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- github.com/advisories/GHSA-87f9-hvmw-gh4pghsaADVISORY
- github.com/mermaid-js/mermaid/commit/64769738d5b59211e1decb471ffbaca8afec51aaghsaWEB
- github.com/mermaid-js/mermaid/commit/a9d9f0d8eb790349121508688cd338253fd80d76ghsaWEB
- github.com/mermaid-js/mermaid/releases/tag/mermaid%4011.15.0ghsaWEB
- github.com/mermaid-js/mermaid/releases/tag/v10.9.6ghsaWEB
- github.com/mermaid-js/mermaid/security/advisories/GHSA-87f9-hvmw-gh4pghsaWEB
News mentions
0No linked articles in our index yet.