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

Mermaid: Improper sanitization of `classDef` in state diagrams leads to HTML injection

CVE-2026-41149

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

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.

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
4e2d512bf5bf

fix: create CSS styles using the CSSOM

https://github.com/mermaid-js/mermaidAlois KlinkApr 8, 2026via ghsa
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,
    
37ff937f1da2

fix: create CSS styles using the CSSOM

https://github.com/mermaid-js/mermaidAlois KlinkMar 20, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.