Mermaid: Improper sanitization of `classDef` in state diagrams leads to HTML injection
Description
Impact
Under the default configuration, Mermaid state diagram's classDef allow DOM injection that escapes the SVG, although <script> tags are removed, preventing XSS.
Proof-of-concept
stateDiagram-v2
classDef xss fill:red</style></svg><style>*{x:x;y:y;overflow:visible!important;contain:none!important;transform:none!important;filter:none!important;clip-path:none!important}</style><div style="x:x;y:y;color:red;font:5em/1 monospace;display:grid;place-items:center;z-index:2147483647;width:100vw;height:100vh;position:fixed;top:0;left:0;background:black">HACKED</div><svg><style>a:b
[*] --> A:::xss
Patches
- v11.15.0 (see 37ff937f1da2e19f882fd1db01235db4d01f4056)
- v10.9.6 (see 4e2d512bf5bf6f9de1a8f0a48da78dc4d09ac4f3)
Workarounds
If you can not update to a patched version, setting `"securityLevel": "sandbox"` will prevent this, by rendering the mermaid diagram in a sandboxed <iframe>.
Credits
Thanks to @zsxsoft from @KeenSecurityLab for reporting this vulnerability.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Mermaid state diagram's classDef allows DOM injection escaping the SVG under default configuration, but script tags are removed preventing XSS; patched in versions 11.15.0 and 10.9.6.
Vulnerability
Description
Mermaid's state diagram parser does not properly sanitize user-supplied CSS in the classDef statement when the securityLevel is not set to "sandbox". This allows an attacker to inject arbitrary CSS and HTML that breaks out of the SVG container, potentially altering the entire page layout. Although <script> tags are removed, preventing classic cross-site scripting (XSS), other HTML elements can be injected, as demonstrated by the proof-of-concept that renders a full-screen overlay.
Exploitation
To exploit this, an attacker needs to supply a malicious state diagram definition, typically through user-controllable fields such as markdown rendering, issue comments, or other text inputs. No authentication is required beyond the ability to provide diagram text. The attack works against any system using Mermaid's state diagram with the default security configuration (which is "strict", but not "sandbox"). The proof-of-concept uses a classDef to inject a style that overrides the SVG's containment and then positions an HTML div over the entire viewport.
Impact
While script execution is blocked, the injected HTML and CSS can be used for content spoofing, phishing, or defacement. An attacker could display fake login prompts, alter visible content, or redirect user interactions. The vulnerability has a high potential for abuse in applications that render user-generated Mermaid diagrams without proper sandboxing.
Mitigation
The issue has been patched in Mermaid versions 11.15.0 and 10.9.6, where CSS is now created using the CSSOM to prevent injection [3][4]. Users unable to update should set securityLevel to "sandbox" in their Mermaid configuration, which renders diagrams inside a sandboxed iframe [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.
| 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
24e2d512bf5bffix: create CSS styles using the CSSOM
7 files changed · +103 −37
cypress/integration/other/ghsa.spec.js+11 −1 modified@@ -1,4 +1,4 @@ -import { urlSnapshotTest, openURLAndVerifyRendering } from '../../helpers/util.ts'; +import { urlSnapshotTest, openURLAndVerifyRendering, imgSnapshotTest } from '../../helpers/util.ts'; describe('CSS injections', () => { it('should not allow CSS injections outside of the diagram', () => { @@ -31,4 +31,14 @@ describe('CSS injections', () => { 'url("https://example.test/3.png")' ); }); + it('should prevent HTML injection via class definitions', () => { + imgSnapshotTest( + `stateDiagram-v2 + classDef xss fill:red</style></svg><style>*{x:x;y:y;overflow:visible!important;contain:none!important;transform:none!important;filter:none!important;clip-path:none!important}</style><div id="pwned" style="x:x;y:y;color:red;font:5em/1 monospace;display:grid;place-items:center;z-index:2147483647;width:100vw;height:100vh;position:fixed;top:0;left:0;background:black">HACKED</div><svg><style>a:b + [*] --> A:::xss + `, + { logLevel: 1 } + ); + cy.get('body > div #pwned').should('not.exist'); + }); });
cypress/integration/rendering/theme.spec.js+10 −0 modified@@ -25,6 +25,16 @@ describe('themeCSS balancing, it', () => { }); }); +it('themeCSS - should work', () => { + const themeCSS = `.nodeLabel { + font-variant-caps: petite-caps; + }`; + imgSnapshotTest("flowchart TD; A['Hello World']", { + themeCSS, + }); + cy.get('.nodeLabel').should('have.css', 'font-variant-caps', 'petite-caps'); +}); + // TODO: Delete/Rename this describe, keeping the inner contents. describe('Pie Chart', () => { // beforeEach(()=>{
docs/config/setup/modules/mermaidAPI.md+6 −6 modified@@ -96,7 +96,7 @@ mermaid.initialize(config); #### Defined in -[mermaidAPI.ts:615](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L615) +[mermaidAPI.ts:633](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L633) ## Functions @@ -127,7 +127,7 @@ Return the last node appended #### Defined in -[mermaidAPI.ts:267](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L267) +[mermaidAPI.ts:285](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L285) --- @@ -153,7 +153,7 @@ the cleaned up svgCode #### Defined in -[mermaidAPI.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L213) +[mermaidAPI.ts:231](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L231) --- @@ -201,7 +201,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:189](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L189) +[mermaidAPI.ts:207](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L207) --- @@ -254,7 +254,7 @@ Put the svgCode into an iFrame. Return the iFrame code #### Defined in -[mermaidAPI.ts:244](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L244) +[mermaidAPI.ts:262](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L262) --- @@ -279,4 +279,4 @@ Remove any existing elements from the given document #### Defined in -[mermaidAPI.ts:320](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L320) +[mermaidAPI.ts:338](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L338)
packages/mermaid/src/diagram-api/types.ts+7 −0 modified@@ -44,7 +44,14 @@ export interface DiagramDB { // It makes it clear we're working with a style class definition, even though defining the type is currently difficult. export interface DiagramStyleClassDef { id: string; + /** + * The styles to apply to the class for HTML rendering. + * These are expected to be CSS property declarations without a trailing semicolon, e.g. `color: red`. + */ styles?: string[]; + /** + * The styles to apply to `<tspan>` elements with the given class. + */ textStyles?: string[]; }
packages/mermaid/src/mermaidAPI.spec.ts+34 −18 modified@@ -51,9 +51,12 @@ import assignWithDepth from './assignWithDepth.js'; // -------------- // Mocks // To mock a module, first define a mock for it, then (if used explicitly in the tests) import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested) -vi.mock('./styles.js', () => { +vi.mock('./styles.js', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const original: typeof import('./styles.js') = await importOriginal(); return { addStylesForDiagram: vi.fn(), + cssStyleSheetToString: vi.fn().mockImplementation(original.cssStyleSheetToString), default: vi.fn().mockImplementation( (_type, userStyles, _options) => ` & .edge-pattern-dashed{ @@ -305,29 +308,38 @@ describe('mermaidAPI', () => { const serif = 'serif'; const sansSerif = 'sans-serif'; const mocked_config_with_htmlLabels: MermaidConfig = { - themeCSS: 'default', + themeCSS: '.default {color: red;}', fontFamily: serif, altFontFamily: sansSerif, htmlLabels: true, }; it('gets the cssStyles from the theme', () => { const styles = createCssStyles(mocked_config_with_htmlLabels, null); - expect(styles).toMatch(/^\ndefault(.*)/); + expect(styles).toContain('.default {color: red;}'); }); + it('gets the fontFamily from the config', () => { const styles = createCssStyles(mocked_config_with_htmlLabels, {}); - expect(styles).toMatch(/(.*)\n:root { --mermaid-font-family: serif(.*)/); + expect(styles).toMatch(/(.*)\n:root {--mermaid-font-family: serif(.*)/); }); + it('gets the alt fontFamily from the config', () => { const styles = createCssStyles(mocked_config_with_htmlLabels, undefined); - expect(styles).toMatch(/(.*)\n:root { --mermaid-alt-font-family: sans-serif(.*)/); + expect(styles).toMatch(/(.*)\n:root {--mermaid-alt-font-family: sans-serif(.*)/); }); describe('there are some classDefs', () => { - const classDef1 = { id: 'classDef1', styles: ['style1-1', 'style1-2'], textStyles: [] }; - const classDef2 = { id: 'classDef2', styles: [], textStyles: ['textStyle2-1'] }; - const classDef3 = { id: 'classDef3', textStyles: ['textStyle3-1', 'textStyle3-2'] }; + const classDef1 = { + id: 'classDef1', + styles: ['prop: style1-1', 'prop-2: style1-2'], + textStyles: [], + }; + const classDef2 = { id: 'classDef2', styles: [], textStyles: ['prop: textStyle2-1'] }; + const classDef3 = { + id: 'classDef3', + textStyles: ['prop: textStyle3-1', 'prop-2: textStyle3-2'], + }; const classDefs = { classDef1, classDef2, classDef3 }; describe('the graph supports classDefs', () => { @@ -352,7 +364,7 @@ describe('mermaidAPI', () => { new RegExp( `\\.classDef1 ${escapeForRegexp( htmlElement - )} \\{ style1-1 !important; style1-2 !important; }` + )} \\{prop: style1-1 !important; prop-2: style1-2 !important;}` ) ); // no CSS styles are created if there are no styles for a classDef @@ -368,14 +380,14 @@ describe('mermaidAPI', () => { function expect_textStyles_matchesHtmlElements(textStyles: string, htmlElement: string) { expect(textStyles).toMatch( new RegExp( - `\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ textStyle2-1 !important; }` + `\\.classDef2 ${escapeForRegexp(htmlElement)} \\{prop: textStyle2-1 !important;}` ) ); expect(textStyles).toMatch( new RegExp( `\\.classDef3 ${escapeForRegexp( htmlElement - )} \\{ textStyle3-1 !important; textStyle3-2 !important; }` + )} \\{prop: textStyle3-1 !important; prop-2: textStyle3-2 !important;}` ) ); @@ -448,21 +460,23 @@ describe('mermaidAPI', () => { describe('createUserStyles', () => { const mockConfig = { - themeCSS: 'default', + themeCSS: '.default {color: red;}', htmlLabels: true, themeVariables: { fontFamily: 'serif' }, }; - const classDef1 = { id: 'classDef1', styles: ['style1-1'], textStyles: [] }; + const classDef1 = { id: 'classDef1', styles: ['prop: style1-1'], textStyles: [] }; + + const divElement = document.body.appendChild(document.createElement('div')); it('gets the css styles created', () => { // @todo TODO if a single function in the module can be mocked, do it for createCssStyles and mock the results. createUserStyles(mockConfig, 'flowchart-v2', { classDef1 }, '#someId'); const expectedStyles = - '\ndefault' + - '\n.classDef1 > * { style1-1 !important; }' + - '\n.classDef1 span { style1-1 !important; }'; + '.default {color: red;}' + + '\n.classDef1 > * {prop: style1-1 !important;}' + + '\n.classDef1 span {prop: style1-1 !important;}'; expect(getStyles).toHaveBeenCalledWith('flowchart-v2', expectedStyles, { fontFamily: 'serif', }); @@ -477,7 +491,9 @@ describe('mermaidAPI', () => { const result = createUserStyles(mockConfig, 'someDiagram', {}, '#someId'); expect(compile).toHaveBeenCalled(); expect(serialize).toHaveBeenCalled(); - expect(result).toEqual('#someId .edge-pattern-dashed{stroke-dasharray:3;}'); + expect(result).toEqual( + '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .default{color:red;}' + ); }); it('should sanitize CSS to avoid unbalanced braces', () => { @@ -498,7 +514,7 @@ describe('mermaidAPI', () => { '#someId' ); expect(result).toEqual( - '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .classDef2>*{color:purple;}#someId .classDef2 span{color:purple;}' + '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .default{color:red;}#someId .classDef2>*{color:purple;}#someId .classDef2 span{color:purple;}' ); }); });
packages/mermaid/src/mermaidAPI.ts+30 −12 modified@@ -21,7 +21,7 @@ import { Diagram, getDiagramFromText as getDiagramFromTextInternal } from './Dia import errorRenderer from './diagrams/error/errorRenderer.js'; import { attachFunctions } from './interactionDb.js'; import { log, setLogLevel } from './logger.js'; -import getStyles from './styles.js'; +import getStyles, { cssStyleSheetToString } from './styles.js'; import theme from './themes/index.js'; import DOMPurify from 'dompurify'; import type { MermaidConfig } from './config.type.js'; @@ -129,7 +129,7 @@ export const cssImportantStyles = ( cssClasses: string[] = [] ): string => { const declarationBlock = sanitizeCss(`{ ${cssClasses.join(' !important; ')} !important; }`); - return `\n.${cssClass} ${element} ${declarationBlock}`; + return `.${cssClass} ${element} ${declarationBlock}`; }; /** @@ -143,20 +143,22 @@ export const createCssStyles = ( config: MermaidConfig, classDefs: Record<string, DiagramStyleClassDef> | null | undefined = {} ): string => { - let cssStyles = ''; + const cssStyles = new CSSStyleSheet(); // user provided theme CSS info // If you add more configuration driven data into the user styles make sure that the value is // sanitized by the sanitize CSS function TODO where is this method? what should be used to replace it? refactor so that it's always sanitized - if (config.themeCSS !== undefined) { - cssStyles += `\n${config.themeCSS}`; - } - if (config.fontFamily !== undefined) { - cssStyles += `\n:root { --mermaid-font-family: ${config.fontFamily}}`; + cssStyles.insertRule( + `:root { --mermaid-font-family: ${config.fontFamily}}`, + cssStyles.cssRules.length + ); } if (config.altFontFamily !== undefined) { - cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`; + cssStyles.insertRule( + `:root { --mermaid-alt-font-family: ${config.altFontFamily}}`, + cssStyles.cssRules.length + ); } // classDefs defined in the diagram text @@ -174,16 +176,32 @@ export const createCssStyles = ( // create the css styles for each cssElement and the styles (only if there are styles) if (!isEmpty(styleClassDef.styles)) { cssElements.forEach((cssElement) => { - cssStyles += cssImportantStyles(styleClassDef.id, cssElement, styleClassDef.styles); + cssStyles.insertRule( + cssImportantStyles(styleClassDef.id, cssElement, styleClassDef.styles), + cssStyles.cssRules.length + ); }); } // create the css styles for the tspan element and the text styles (only if there are textStyles) if (!isEmpty(styleClassDef.textStyles)) { - cssStyles += cssImportantStyles(styleClassDef.id, 'tspan', styleClassDef.textStyles); + cssStyles.insertRule( + cssImportantStyles(styleClassDef.id, 'tspan', styleClassDef.textStyles), + cssStyles.cssRules.length + ); } } } - return cssStyles; + + let cssString = ''; + if (config.themeCSS !== undefined) { + /** + * Ideally we'd do a `CSSStyleSheet.replaceSync`, but it's not supported + * in some older browsers and in JSDOM. + */ + cssString += `${config.themeCSS}\n`; + } + + return cssString + cssStyleSheetToString(cssStyles); }; export const createUserStyles = (
packages/mermaid/src/styles.ts+5 −0 modified@@ -4,6 +4,11 @@ import type { DiagramStylesProvider } from './diagram-api/types.js'; const themes: Record<string, DiagramStylesProvider> = {}; +export function cssStyleSheetToString(cssStyleSheet: CSSStyleSheet): string { + // @ts-ignore -- Typedoc is throwing Type 'CSSRuleList' must have a '[Symbol.iterator]()' method error + return [...cssStyleSheet.cssRules].map((rule) => rule.cssText).join('\n'); +} + const getStyles = ( type: string, userStyles: string,
37ff937f1da2fix: create CSS styles using the CSSOM
11 files changed · +115 −44
.changeset/cold-numbers-taste.md+7 −0 added@@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix: create CSS styles using the CSSOM + +This removes some invalid CSS and normalizes some CSS formatting.
cypress/integration/other/ghsa.spec.js+11 −1 modified@@ -1,4 +1,4 @@ -import { urlSnapshotTest, openURLAndVerifyRendering } from '../../helpers/util.ts'; +import { urlSnapshotTest, openURLAndVerifyRendering, imgSnapshotTest } from '../../helpers/util.ts'; describe('CSS injections', () => { it('should not allow CSS injections outside of the diagram', () => { @@ -31,4 +31,14 @@ describe('CSS injections', () => { 'url("https://example.test/3.png")' ); }); + it('should prevent HTML injection via class definitions', () => { + imgSnapshotTest( + `stateDiagram-v2 + classDef xss fill:red</style></svg><style>*{x:x;y:y;overflow:visible!important;contain:none!important;transform:none!important;filter:none!important;clip-path:none!important}</style><div id="pwned" style="x:x;y:y;color:red;font:5em/1 monospace;display:grid;place-items:center;z-index:2147483647;width:100vw;height:100vh;position:fixed;top:0;left:0;background:black">HACKED</div><svg><style>a:b + [*] --> A:::xss + `, + { logLevel: 1 } + ); + cy.get('body > div #pwned').should('not.exist'); + }); });
cypress/integration/rendering/theme.spec.js+10 −0 modified@@ -23,6 +23,16 @@ describe('themeCSS balancing, it', () => { }); }); +it('themeCSS - should work', () => { + const themeCSS = `.nodeLabel { + font-variant-caps: petite-caps; + }`; + imgSnapshotTest("flowchart TD; A['Hello World']", { + themeCSS, + }); + cy.get('.nodeLabel').should('have.css', 'font-variant-caps', 'petite-caps'); +}); + // TODO: Delete/Rename this describe, keeping the inner contents. describe('Pie Chart', () => { // beforeEach(()=>{
docs/config/setup/mermaid/interfaces/ExternalDiagramDefinition.md+4 −4 modified@@ -10,28 +10,28 @@ # Interface: ExternalDiagramDefinition -Defined in: [packages/mermaid/src/diagram-api/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L97) +Defined in: [packages/mermaid/src/diagram-api/types.ts:104](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L104) ## Properties ### detector > **detector**: `DiagramDetector` -Defined in: [packages/mermaid/src/diagram-api/types.ts:99](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L99) +Defined in: [packages/mermaid/src/diagram-api/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L106) --- ### id > **id**: `string` -Defined in: [packages/mermaid/src/diagram-api/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L98) +Defined in: [packages/mermaid/src/diagram-api/types.ts:105](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L105) --- ### loader > **loader**: `DiagramLoader` -Defined in: [packages/mermaid/src/diagram-api/types.ts:100](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L100) +Defined in: [packages/mermaid/src/diagram-api/types.ts:107](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L107)
docs/config/setup/mermaid/interfaces/RenderResult.md+4 −4 modified@@ -10,15 +10,15 @@ # Interface: RenderResult -Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116) +Defined in: [packages/mermaid/src/types.ts:129](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L129) ## Properties ### bindFunctions()? > `optional` **bindFunctions**: (`element`) => `void` -Defined in: [packages/mermaid/src/types.ts:134](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L134) +Defined in: [packages/mermaid/src/types.ts:147](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L147) Bind function to be called after the svg has been inserted into the DOM. This is necessary for adding event listeners to the elements in the svg. @@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present. > **diagramType**: `string` -Defined in: [packages/mermaid/src/types.ts:124](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L124) +Defined in: [packages/mermaid/src/types.ts:137](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L137) The diagram type, e.g. 'flowchart', 'sequence', etc. @@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc. > **svg**: `string` -Defined in: [packages/mermaid/src/types.ts:120](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L120) +Defined in: [packages/mermaid/src/types.ts:133](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L133) The svg code for the rendered graph.
docs/config/setup/mermaid/type-aliases/SVGGroup.md+1 −1 modified@@ -12,4 +12,4 @@ > **SVGGroup** = `d3.Selection`<`SVGGElement`, `unknown`, `Element` | `null`, `unknown`> -Defined in: [packages/mermaid/src/diagram-api/types.ts:131](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L131) +Defined in: [packages/mermaid/src/diagram-api/types.ts:138](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L138)
docs/config/setup/mermaid/type-aliases/SVG.md+1 −1 modified@@ -12,4 +12,4 @@ > **SVG** = `d3.Selection`<`SVGSVGElement`, `unknown`, `Element` | `null`, `unknown`> -Defined in: [packages/mermaid/src/diagram-api/types.ts:129](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L129) +Defined in: [packages/mermaid/src/diagram-api/types.ts:136](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/diagram-api/types.ts#L136)
packages/mermaid/src/diagram-api/types.ts+7 −0 modified@@ -64,7 +64,14 @@ export type DiagramDBBase<T extends BaseDiagramConfig> = { // It makes it clear we're working with a style class definition, even though defining the type is currently difficult. export interface DiagramStyleClassDef { id: string; + /** + * The styles to apply to the class for HTML rendering. + * These are expected to be CSS property declarations without a trailing semicolon, e.g. `color: red`. + */ styles?: string[]; + /** + * The styles to apply to `<tspan>` elements with the given class. + */ textStyles?: string[]; }
packages/mermaid/src/mermaidAPI.spec.ts+33 −18 modified@@ -16,9 +16,11 @@ import * as configApi from './config.js'; // -------------- // Mocks // To mock a module, first define a mock for it, then (if used explicitly in the tests) import it. Be sure the path points to exactly the same file as is imported in mermaidAPI (the module being tested) -vi.mock(import('./styles.js'), () => { +vi.mock(import('./styles.js'), async (importOriginal) => { + const original = await importOriginal(); return { addStylesForDiagram: vi.fn(), + cssStyleSheetToString: vi.fn().mockImplementation(original.cssStyleSheetToString), default: vi.fn().mockImplementation( (_type, userStyles, _options) => ` & .edge-pattern-dashed{ @@ -241,29 +243,38 @@ describe('mermaidAPI', () => { const serif = 'serif'; const sansSerif = 'sans-serif'; const mocked_config_with_htmlLabels: MermaidConfig = { - themeCSS: 'default', + themeCSS: '.default {color: red;}', fontFamily: serif, altFontFamily: sansSerif, htmlLabels: true, }; it('gets the cssStyles from the theme', () => { const styles = createCssStyles(mocked_config_with_htmlLabels, null); - expect(styles).toMatch(/^\ndefault(.*)/); + expect(styles).toContain('.default {color: red;}'); }); + it('gets the fontFamily from the config', () => { const styles = createCssStyles(mocked_config_with_htmlLabels, new Map()); - expect(styles).toMatch(/(.*)\n:root { --mermaid-font-family: serif(.*)/); + expect(styles).toMatch(/(.*)\n:root {--mermaid-font-family: serif(.*)/); }); + it('gets the alt fontFamily from the config', () => { const styles = createCssStyles(mocked_config_with_htmlLabels, undefined); - expect(styles).toMatch(/(.*)\n:root { --mermaid-alt-font-family: sans-serif(.*)/); + expect(styles).toMatch(/(.*)\n:root {--mermaid-alt-font-family: sans-serif(.*)/); }); describe('there are some classDefs', () => { - const classDef1 = { id: 'classDef1', styles: ['style1-1', 'style1-2'], textStyles: [] }; - const classDef2 = { id: 'classDef2', styles: [], textStyles: ['textStyle2-1'] }; - const classDef3 = { id: 'classDef3', textStyles: ['textStyle3-1', 'textStyle3-2'] }; + const classDef1 = { + id: 'classDef1', + styles: ['prop: style1-1', 'prop-2: style1-2'], + textStyles: [], + }; + const classDef2 = { id: 'classDef2', styles: [], textStyles: ['prop: textStyle2-1'] }; + const classDef3 = { + id: 'classDef3', + textStyles: ['prop: textStyle3-1', 'prop-2: textStyle3-2'], + }; const classDefs = { classDef1, classDef2, classDef3 }; describe('the graph supports classDefs', () => { @@ -288,7 +299,7 @@ describe('mermaidAPI', () => { new RegExp( `\\.classDef1 ${escapeForRegexp( htmlElement - )} \\{ style1-1 !important; style1-2 !important; }` + )} \\{prop: style1-1 !important; prop-2: style1-2 !important;}` ) ); // no CSS styles are created if there are no styles for a classDef @@ -304,14 +315,14 @@ describe('mermaidAPI', () => { function expect_textStyles_matchesHtmlElements(textStyles: string, htmlElement: string) { expect(textStyles).toMatch( new RegExp( - `\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ textStyle2-1 !important; }` + `\\.classDef2 ${escapeForRegexp(htmlElement)} \\{prop: textStyle2-1 !important;}` ) ); expect(textStyles).toMatch( new RegExp( `\\.classDef3 ${escapeForRegexp( htmlElement - )} \\{ textStyle3-1 !important; textStyle3-2 !important; }` + )} \\{prop: textStyle3-1 !important; prop-2: textStyle3-2 !important;}` ) ); @@ -388,21 +399,23 @@ describe('mermaidAPI', () => { describe('createUserStyles', () => { const mockConfig = { - themeCSS: 'default', + themeCSS: '.default {color: red;}', htmlLabels: true, themeVariables: { fontFamily: 'serif' }, }; - const classDef1 = { id: 'classDef1', styles: ['style1-1'], textStyles: [] }; + const classDef1 = { id: 'classDef1', styles: ['prop: style1-1'], textStyles: [] }; + + const divElement = document.body.appendChild(document.createElement('div')); it('gets the css styles created', () => { // @todo TODO if a single function in the module can be mocked, do it for createCssStyles and mock the results. createUserStyles(mockConfig, 'flowchart-v2', new Map([['classDef1', classDef1]]), '#someId'); const expectedStyles = - '\ndefault' + - '\n.classDef1 > * { style1-1 !important; }' + - '\n.classDef1 span { style1-1 !important; }'; + '.default {color: red;}' + + '\n.classDef1 > * {prop: style1-1 !important;}' + + '\n.classDef1 span {prop: style1-1 !important;}'; expect(getStyles).toHaveBeenCalledWith( 'flowchart-v2', expectedStyles, @@ -422,7 +435,9 @@ describe('mermaidAPI', () => { const result = createUserStyles(mockConfig, 'someDiagram', new Map(), '#someId'); expect(compile).toHaveBeenCalled(); expect(serialize).toHaveBeenCalled(); - expect(result).toEqual('#someId .edge-pattern-dashed{stroke-dasharray:3;}'); + expect(result).toEqual( + '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .default{color:red;}' + ); }); it('should sanitize CSS to avoid unbalanced braces', () => { @@ -443,7 +458,7 @@ describe('mermaidAPI', () => { '#someId' ); expect(result).toEqual( - '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .classDef2>*{color:purple;}#someId .classDef2 span{color:purple;}' + '#someId .edge-pattern-dashed{stroke-dasharray:3;}#someId .default{color:red;}#someId .classDef2>*{color:purple;}#someId .classDef2 span{color:purple;}' ); }); });
packages/mermaid/src/mermaidAPI.ts+33 −15 modified@@ -20,7 +20,7 @@ import errorRenderer from './diagrams/error/errorRenderer.js'; import { attachFunctions } from './interactionDb.js'; import { log, setLogLevel } from './logger.js'; import { preprocessDiagram } from './preprocess.js'; -import getStyles from './styles.js'; +import getStyles, { cssStyleSheetToString } from './styles.js'; import theme from './themes/index.js'; import type { D3HtmlSelection, @@ -104,7 +104,7 @@ export const cssImportantStyles = ( cssClasses: string[] = [] ): string => { const declarationBlock = sanitizeCss(`{ ${cssClasses.join(' !important; ')} !important; }`); - return `\n.${cssClass} ${element} ${declarationBlock}`; + return `.${cssClass} ${element} ${declarationBlock}`; }; /** @@ -118,20 +118,22 @@ export const createCssStyles = ( config: MermaidConfig, classDefs: Map<string, DiagramStyleClassDef> | null | undefined = new Map() ): string => { - let cssStyles = ''; + const cssStyles = new CSSStyleSheet(); // user provided theme CSS info // If you add more configuration driven data into the user styles make sure that the value is // sanitized by the sanitize CSS function TODO where is this method? what should be used to replace it? refactor so that it's always sanitized - if (config.themeCSS !== undefined) { - cssStyles += `\n${config.themeCSS}`; - } - if (config.fontFamily !== undefined) { - cssStyles += `\n:root { --mermaid-font-family: ${config.fontFamily}}`; + cssStyles.insertRule( + `:root { --mermaid-font-family: ${config.fontFamily}}`, + cssStyles.cssRules.length + ); } if (config.altFontFamily !== undefined) { - cssStyles += `\n:root { --mermaid-alt-font-family: ${config.altFontFamily}}`; + cssStyles.insertRule( + `:root { --mermaid-alt-font-family: ${config.altFontFamily}}`, + cssStyles.cssRules.length + ); } // classDefs defined in the diagram text @@ -148,20 +150,36 @@ export const createCssStyles = ( // create the css styles for each cssElement and the styles (only if there are styles) if (!isEmpty(styleClassDef.styles)) { cssElements.forEach((cssElement) => { - cssStyles += cssImportantStyles(styleClassDef.id, cssElement, styleClassDef.styles); + cssStyles.insertRule( + cssImportantStyles(styleClassDef.id, cssElement, styleClassDef.styles), + cssStyles.cssRules.length + ); }); } // create the css styles for the tspan element and the text styles (only if there are textStyles) if (!isEmpty(styleClassDef.textStyles)) { - cssStyles += cssImportantStyles( - styleClassDef.id, - 'tspan', - (styleClassDef?.textStyles || []).map((s) => s.replace('color', 'fill')) + cssStyles.insertRule( + cssImportantStyles( + styleClassDef.id, + 'tspan', + (styleClassDef?.textStyles || []).map((s) => s.replace('color', 'fill')) + ), + cssStyles.cssRules.length ); } }); } - return cssStyles; + + let cssString = ''; + if (config.themeCSS !== undefined) { + /** + * Ideally we'd do a `CSSStyleSheet.replaceSync`, but it's not supported + * in some older browsers and in JSDOM. + */ + cssString += `${config.themeCSS}\n`; + } + + return cssString + cssStyleSheetToString(cssStyles); }; export const createUserStyles = (
packages/mermaid/src/styles.ts+4 −0 modified@@ -4,6 +4,10 @@ import type { DiagramStylesProvider } from './diagram-api/types.js'; const themes: Record<string, DiagramStylesProvider> = {}; +export function cssStyleSheetToString(cssStyleSheet: CSSStyleSheet): string { + return [...cssStyleSheet.cssRules].map((rule) => rule.cssText).join('\n'); +} + const getStyles = ( type: string, userStyles: string,
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
7- github.com/advisories/GHSA-ghcm-xqfw-q4vrghsaADVISORY
- github.com/mermaid-js/mermaid/commit/37ff937f1da2e19f882fd1db01235db4d01f4056ghsaWEB
- github.com/mermaid-js/mermaid/commit/4e2d512bf5bf6f9de1a8f0a48da78dc4d09ac4f3ghsaWEB
- 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-ghcm-xqfw-q4vrghsaWEB
- mermaid.js.org/config/schema-docs/config.htmlghsaWEB
News mentions
0No linked articles in our index yet.