Low severityNVD Advisory· Published Aug 29, 2022· Updated Apr 22, 2025
Improper Control of Generation of Code ('Code Injection') in mdx-mermaid
CVE-2022-36036
Description
mdx-mermaid provides plug and play access to Mermaid in MDX. There is a potential for an arbitrary javascript injection in versions less than 1.3.0 and 2.0.0-rc1. Modify any mermaid code blocks with arbitrary code and it will execute when the component is loaded by MDXjs. This vulnerability was patched in version(s) 1.3.0 and 2.0.0-rc2. There are currently no known workarounds.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mdx-mermaidnpm | < 1.3.0 | 1.3.0 |
mdx-mermaidnpm | >= 2.0.0-rc1, < 2.0.0-rc2 | 2.0.0-rc2 |
Affected products
1- Range: < 1.3.0
Patches
1f2b99386660fMerge pull request from GHSA-rvgm-35jw-q628
7 files changed · +710 −257
package.json+2 −1 modified@@ -53,6 +53,7 @@ "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-typescript": "^8.3.4", + "@testing-library/react": "^11.1.0", "@types/jest": "^27.4.0", "@types/mermaid": "^8.2.7", "@types/react": "^17.0.38", @@ -69,7 +70,7 @@ "jest": "^27.4.7", "mermaid": "^8.0.0", "react": "^17.0.1", - "react-test-renderer": "^17.0.2", + "react-dom": "^17.0.0", "rimraf": "^3.0.2", "rollup": "^2.78.1", "ts-jest": "^27.1.2",
src/mdxast-mermaid.spec.ts+154 −30 modified@@ -24,7 +24,24 @@ function createTestCompiler (config?: Config) { test('No mermaid', async () => { const mdxCompiler = createTestCompiler() const result = await mdxCompiler.process('# Heading 1\n\nNo Mermaid diagram :(') - expect(result.contents).toEqual('\n\n\nconst layoutProps = {\n \n};\nconst MDXLayout = "wrapper"\nexport default function MDXContent({\n components,\n ...props\n}) {\n return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">\n <h1>{`Heading 1`}</h1>\n <p>{`No Mermaid diagram :(`}</p>\n </MDXLayout>;\n}\n\n;\nMDXContent.isMDXComponent = true;') + expect(result.contents).toEqual(`import { Mermaid } from 'mdx-mermaid/lib/Mermaid'; + + +const layoutProps = {\n \n}; +const MDXLayout = "wrapper" +export default function MDXContent({ + components, + ...props +}) { + return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> + + <h1>{\`Heading 1\`}</h1> + <p>{\`No Mermaid diagram :(\`}</p> + </MDXLayout>; +} + +; +MDXContent.isMDXComponent = true;`) }) test('Basic', async () => { @@ -37,7 +54,26 @@ graph TD; B-->D; C-->D; \`\`\``) - expect(result.contents).toEqual("import { Mermaid } from 'mdx-mermaid/lib/Mermaid';\n\n\nconst layoutProps = {\n \n};\nconst MDXLayout = \"wrapper\"\nexport default function MDXContent({\n components,\n ...props\n}) {\n return <MDXLayout {...layoutProps} {...props} components={components} mdxType=\"MDXLayout\">\n\n <h1>{`Heading 1`}</h1>\n <Mermaid config={{}} chart={`graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;`} mdxType=\"Mermaid\" />\n </MDXLayout>;\n}\n\n;\nMDXContent.isMDXComponent = true;") + expect(result.contents).toEqual(`import { Mermaid } from 'mdx-mermaid/lib/Mermaid'; + + +const layoutProps = {\n \n}; +const MDXLayout = "wrapper" +export default function MDXContent({ + components, + ...props +}) { + return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> + + <h1>{\`Heading 1\`}</h1> + <Mermaid {...{ + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> + </MDXLayout>; +} + +; +MDXContent.isMDXComponent = true;`) }) test('Existing import', async () => { @@ -50,7 +86,26 @@ graph TD; B-->D; C-->D; \`\`\``) - expect(result.contents).toEqual("import { Mermaid } from 'mdx-mermaid/lib/Mermaid';\n\n\nconst layoutProps = {\n \n};\nconst MDXLayout = \"wrapper\"\nexport default function MDXContent({\n components,\n ...props\n}) {\n return <MDXLayout {...layoutProps} {...props} components={components} mdxType=\"MDXLayout\">\n\n <h1>{`Heading 1`}</h1>\n <Mermaid config={{}} chart={`graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;`} mdxType=\"Mermaid\" />\n </MDXLayout>;\n}\n\n;\nMDXContent.isMDXComponent = true;") + expect(result.contents).toEqual(`import { Mermaid } from 'mdx-mermaid/lib/Mermaid'; + + +const layoutProps = {\n \n}; +const MDXLayout = "wrapper" +export default function MDXContent({ + components, + ...props +}) { + return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> + + <h1>{\`Heading 1\`}</h1> + <Mermaid {...{ + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> + </MDXLayout>; +} + +; +MDXContent.isMDXComponent = true;`) }) test('Existing import from ts exports(without /lib)', async () => { @@ -63,7 +118,26 @@ graph TD; B-->D; C-->D; \`\`\``) - expect(result.contents).toEqual("import { Mermaid } from 'mdx-mermaid/Mermaid';\n\n\nconst layoutProps = {\n \n};\nconst MDXLayout = \"wrapper\"\nexport default function MDXContent({\n components,\n ...props\n}) {\n return <MDXLayout {...layoutProps} {...props} components={components} mdxType=\"MDXLayout\">\n\n <h1>{`Heading 1`}</h1>\n <Mermaid config={{}} chart={`graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;`} mdxType=\"Mermaid\" />\n </MDXLayout>;\n}\n\n;\nMDXContent.isMDXComponent = true;") + expect(result.contents).toEqual(`import { Mermaid } from 'mdx-mermaid/Mermaid'; + + +const layoutProps = {\n \n}; +const MDXLayout = "wrapper" +export default function MDXContent({ + components, + ...props +}) { + return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> + + <h1>{\`Heading 1\`}</h1> + <Mermaid {...{ + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> + </MDXLayout>; +} + +; +MDXContent.isMDXComponent = true;`) }) test('Other imports', async () => { @@ -76,7 +150,28 @@ graph TD; B-->D; C-->D; \`\`\``) - expect(result.contents).toEqual("import { Mermaid } from 'mdx-mermaid/lib/Mermaid';\nimport { A } from 'a';\n\n\nconst layoutProps = {\n \n};\nconst MDXLayout = \"wrapper\"\nexport default function MDXContent({\n components,\n ...props\n}) {\n return <MDXLayout {...layoutProps} {...props} components={components} mdxType=\"MDXLayout\">\n\n\n <h1>{`Heading 1`}</h1>\n <Mermaid config={{}} chart={`graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;`} mdxType=\"Mermaid\" />\n </MDXLayout>;\n}\n\n;\nMDXContent.isMDXComponent = true;") + expect(result.contents).toEqual(`import { Mermaid } from 'mdx-mermaid/lib/Mermaid'; +import { A } from 'a'; + + +const layoutProps = {\n \n}; +const MDXLayout = "wrapper" +export default function MDXContent({ + components, + ...props +}) { + return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> + + + <h1>{\`Heading 1\`}</h1> + <Mermaid {...{ + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> + </MDXLayout>; +} + +; +MDXContent.isMDXComponent = true;`) }) test('Other imports component', async () => { @@ -101,7 +196,7 @@ export default function MDXContent({ <h1>{\`Heading 1\`}</h1> - <Mermaid config={{}} chart={\`graph TD; + <Mermaid chart={\`graph TD; A-->B; A-->C; B-->D; @@ -144,7 +239,7 @@ export default function MDXContent({ <h1>{\`Heading 1\`}</h1> <A mdxType="A">Hi</A> <h2>{\`Heading 2\`}</h2> - <Mermaid config={{}} chart={\`graph TD; + <Mermaid chart={\`graph TD; A-->B; A-->C; B-->D; @@ -178,15 +273,10 @@ export default function MDXContent({ return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> <h1>{\`Heading 1\`}</h1> - <Mermaid config={{ - "mermaid": { - "theme": "dark" - } - }} chart={\`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;\`} mdxType="Mermaid" /> + <Mermaid {...{ + "config": "{\\"mermaid\\":{\\"theme\\":\\"dark\\"}}", + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> </MDXLayout>; } @@ -214,11 +304,7 @@ export default function MDXContent({ return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> <h1>{\`Heading 1\`}</h1> - <Mermaid config={{ - "mermaid": { - "theme": "dark" - } - }} chart={\`graph TD; + <Mermaid chart={\`graph TD; A-->B; A-->C; B-->D; @@ -257,15 +343,10 @@ export default function MDXContent({ return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> <h1>{\`Heading 1\`}</h1> - <Mermaid config={{ - "mermaid": { - "theme": "dark" - } - }} chart={\`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;\`} mdxType="Mermaid" /> + <Mermaid {...{ + "config": "{\\"mermaid\\":{\\"theme\\":\\"dark\\"}}", + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> <Mermaid chart={\`graph TD; E-->F; E-->G; @@ -277,3 +358,46 @@ export default function MDXContent({ ; MDXContent.isMDXComponent = true;`) }) + +test('Multiple code block', async () => { + const mdxCompiler = createTestCompiler({ mermaid: { theme: 'dark' } }) + const result = await mdxCompiler.process(`# Heading 1\n +\`\`\`mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +\`\`\` +\`\`\`mermaid +graph TD; +E-->F; +E-->G; +F-->H; +G-->H; +\`\`\``) + expect(result.contents).toEqual(`import { Mermaid } from 'mdx-mermaid/lib/Mermaid'; + + +const layoutProps = {\n \n}; +const MDXLayout = "wrapper" +export default function MDXContent({ + components, + ...props +}) { + return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout"> + + <h1>{\`Heading 1\`}</h1> + <Mermaid {...{ + "config": "{\\"mermaid\\":{\\"theme\\":\\"dark\\"}}", + "chart": "graph TD;\\n A-->B;\\n A-->C;\\n B-->D;\\n C-->D;" + }} mdxType="Mermaid"></Mermaid> + <Mermaid {...{ + "chart": "graph TD;\\nE-->F;\\nE-->G;\\nF-->H;\\nG-->H;" + }} mdxType="Mermaid"></Mermaid> + </MDXLayout>; +} + +; +MDXContent.isMDXComponent = true;`) +})
src/mdxast-mermaid.ts+10 −18 modified@@ -53,28 +53,20 @@ export default function plugin (config?: Config) { }) // Replace each Mermaid code block with the Mermaid component - instances.forEach(([node, index, parent]) => { + instances.forEach(([node, index, parent], i) => { parent.children.splice(index, 1, { - type: 'jsx', - value: `<Mermaid chart={\`${node.value}\`}/>`, - position: node.position + type: 'mermaidCodeBlock', + data: { + hName: 'Mermaid', + hProperties: { + config: i > 0 ? undefined : JSON.stringify(config), + chart: node.value + } + } }) }) - // Look for any components - visit<Literal<string> & { type: 'jsx' }>(ast, { type: 'jsx' }, (node, index, parent) => { - if (/.*<Mermaid.*/.test(node.value)) { - // If the component doesn't have config - if (!/.*config={.*/.test(node.value)) { - const index = node.value.indexOf('<Mermaid') + 8 - node.value = node.value.substring(0, index) + - ` config={${JSON.stringify(config || {})}}` + - node.value.substring(index) - } - insertImport(ast) - return visit.EXIT - } - }) + insertImport(ast) return ast } }
src/Mermaid.spec.tsx+92 −152 modified@@ -7,9 +7,8 @@ * This source code is licensed under the MIT license found in the * license file in the root directory of this source tree. */ -import mermaid from 'mermaid' import React from 'react' -import renderer from 'react-test-renderer' +import { act, render, RenderResult } from '@testing-library/react' import { Mermaid } from './Mermaid' import { DARK_THEME_KEY, @@ -18,185 +17,126 @@ import { } from './theme.helper' import * as ThemeHelper from './theme.helper' -async function waitFor (ms: number) { - return new Promise<void>(resolve => { - setTimeout(() => resolve(), ms) - }) -} - jest.mock('mermaid') +// eslint-disable-next-line import/first +import mermaid from 'mermaid' + +const getThemeSpy = jest.spyOn(ThemeHelper, 'getTheme') + +const diagram = `graph TD; +A-->B; +A-->C; +B-->D; +C-->D;` + afterEach(() => { jest.clearAllMocks() }) +const removeUniqueness = (element: Element) => { + element.querySelectorAll('style').forEach((v) => v.remove()) + element.querySelectorAll('svg').forEach((v) => { + v.removeAttribute('id') + v.parentElement!.removeAttribute('id') + }) +} + +const expectMermaidMatch = (result: RenderResult) => { + removeUniqueness(result.baseElement) + expect(result.baseElement.parentElement).toMatchSnapshot() + return result +} + it('renders without diagram', () => { - const component = renderer.create(<Mermaid chart={''} config={{}} />) - expect(mermaid.initialize).toBeCalledTimes(0) - expect(mermaid.render).toBeCalledTimes(0) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) + expectMermaidMatch(render(<Mermaid chart={''} config={{}} />)) }) it('renders with diagram', () => { - const component = renderer.create(<Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{}} />) - expect(mermaid.initialize).toBeCalledTimes(0) - expect(mermaid.render).toBeCalledTimes(0) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) + expectMermaidMatch(render(<svg><Mermaid chart={diagram} config={{}} /></svg>)) }) -it('initializes only once', async () => { - const component = renderer.create(<> - <Mermaid chart={'foo'} config={{}} /> - <Mermaid chart={'bar'} /> - </>) - expect(mermaid.initialize).toBeCalledTimes(0) - expect(mermaid.render).toBeCalledTimes(0) - await waitFor(1000) - component.update() - expect(mermaid.render).toBeCalledTimes(2) +it('renders with diagram change', () => { + const config = {} + jest.useFakeTimers() + const view = expectMermaidMatch(render(<Mermaid chart={diagram} config={config} />)) + view.rerender(<Mermaid chart={`graph TD; +D-->C; +D-->B; +C-->A; +B-->A;`} config={config} />) + jest.advanceTimersByTime(1000) + expectMermaidMatch(view) + jest.useRealTimers() + expect(mermaid.contentLoaded).toBeCalledTimes(1) expect(mermaid.initialize).toBeCalledTimes(1) - component.update() - expect(mermaid.render).toBeCalledTimes(2) +}) + +it('initializes only once', () => { + expectMermaidMatch(render(<> + <Mermaid chart={'foo'} config={{}} /> + <Mermaid chart={'bar'} /> + </>)) + expect(mermaid.contentLoaded).toBeCalledTimes(1) expect(mermaid.initialize).toBeCalledTimes(1) }) it('renders with mermaid config', () => { - const component = renderer.create(<Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{ mermaid: { theme: 'dark' } } } />) - expect(mermaid.initialize).toBeCalledTimes(0) - expect(mermaid.render).toBeCalledTimes(0) - component.update() - expect(mermaid.render).toHaveBeenCalled() - expect(mermaid.initialize).toHaveBeenNthCalledWith(1, { startOnLoad: true, theme: 'dark' }) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) + expectMermaidMatch(render(<Mermaid chart={diagram} config={{ mermaid: { theme: 'dark' } }} />)) + expect(mermaid.contentLoaded).toBeCalledTimes(1) + expect(mermaid.initialize).toBeCalledWith({ startOnLoad: true, theme: 'dark' }) }) -it('re-renders mermaid theme on html data-theme attribute change', async () => { - const component = renderer.create( - <html data-theme='light'> - <Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{}} /> - </html>) - expect(mermaid.initialize).toBeCalledTimes(0) - expect(mermaid.render).toBeCalledTimes(0) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) - component.update() - expect(mermaid.render).toBeCalledTimes(1) - expect(mermaid.initialize).toBeCalledTimes(1) +it('renders with mermaid config change', () => { + const view = expectMermaidMatch(render(<Mermaid chart={diagram} config={{ mermaid: { theme: 'dark' } }} />)) + view.baseElement.querySelectorAll('div.mermaid').forEach((v) => { + v.setAttribute('data-processed', 'true') + }) + expect(mermaid.contentLoaded).toBeCalledTimes(1) + expect(mermaid.initialize).toBeCalledWith({ startOnLoad: true, theme: 'dark' }) + view.rerender(<Mermaid chart={diagram} config={{ mermaid: { theme: 'forest' } }} />) + // await waitFor(1000) + expectMermaidMatch(view) + expect(mermaid.contentLoaded).toBeCalledTimes(2) + expect(mermaid.initialize).toHaveBeenNthCalledWith(2, { startOnLoad: true, theme: 'forest' }) +}) - component.update( - <html data-theme='dark'> - <Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{}} /> - </html>) +it('renders with string mermaid config', () => { + expectMermaidMatch(render(<Mermaid chart={diagram} config={JSON.stringify({ mermaid: { theme: 'dark' } })} />)) + expect(mermaid.contentLoaded).toBeCalledTimes(1) + expect(mermaid.initialize).toBeCalledWith({ startOnLoad: true, theme: 'dark' }) +}) - // Time for mutation observer to notice change. - await waitFor(2000) +it('re-renders mermaid theme on html data-theme attribute change', () => { + const component = render( + <Mermaid chart={diagram} config={{}} />) - expect(mermaid.render).toBeCalledTimes(2) - expect(mermaid.initialize).toBeCalledTimes(2) -}) + expectMermaidMatch(component) + expect(mermaid.contentLoaded).toBeCalledTimes(1) + expect(mermaid.initialize).toBeCalledTimes(1) + expect(getThemeSpy).toBeCalledTimes(1) -it('renders the output of mermaid into the div', async () => { - const expectedOutput = 'mermaid output' - mermaid.render = jest.fn((_, __, cb) => { - if (cb) cb(expectedOutput, () => 0) - return expectedOutput - }) + act(() => document.querySelector('html')!.setAttribute(HTML_THEME_ATTRIBUTE, DARK_THEME_KEY)) - let component: any - renderer.act(() => { - component = renderer.create( - <Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{}} /> - ) - }) + expectMermaidMatch(component) + expect(mermaid.contentLoaded).toBeCalledTimes(1) expect(mermaid.initialize).toBeCalledTimes(1) - expect(mermaid.render).toBeCalledTimes(1) - expect(component.toJSON()).toMatchSnapshot() -}) + expect(getThemeSpy).toBeCalledTimes(1) -describe('changing the theme at runtime', () => { - let useRefSpy: jest.SpyInstance - let html: HTMLHtmlElement + act(() => document.querySelector('html')!.setAttribute(HTML_THEME_ATTRIBUTE, LIGHT_THEME_KEY)) - beforeEach(() => { - html = document.createElement('html') - html.setAttribute(HTML_THEME_ATTRIBUTE, LIGHT_THEME_KEY) - useRefSpy = jest.spyOn(document, 'querySelector').mockReturnValue(html) - }) + expectMermaidMatch(component) +}) - afterEach(() => { - expect(useRefSpy).toHaveBeenCalled() - }) +it('does not react to non-theme attribute changes of html', () => { + const component = render(<Mermaid chart={diagram} config={{}} />) - it('reacts to changed theme', async () => { - const getThemeSpy = jest.spyOn(ThemeHelper, 'getTheme') - renderer.act(() => { - renderer.create( - <Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{}} /> - ) - }) - - await renderer.act(async () => { - html.setAttribute(HTML_THEME_ATTRIBUTE, DARK_THEME_KEY) - await waitFor(1000) - }) - - expect(getThemeSpy.mock.calls.length).toBeGreaterThan(2) - }) + expectMermaidMatch(component) + expect(mermaid.contentLoaded).toBeCalledTimes(1) + expect(mermaid.initialize).toBeCalledTimes(1) - it('does not react to non-theme attribute changes of html', async () => { - const getThemeSpy = jest.spyOn(ThemeHelper, 'getTheme') - renderer.act(() => { - renderer.create( - <Mermaid chart={`graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`} config={{}} /> - ) - }) - - await renderer.act(async () => { - html.setAttribute('manifest', 'some-value') - await waitFor(1000) - }) - expect(getThemeSpy).toHaveBeenCalledTimes(2) - }) + act(() => document.querySelector('html')!.setAttribute('manifest', 'some-value')) + + expectMermaidMatch(component) })
src/Mermaid.tsx+22 −28 modified@@ -5,19 +5,12 @@ * license file in the root directory of this source tree. */ -import React, { useEffect, useState, ReactElement } from 'react' +import React, { useEffect, useState, ReactElement, useMemo } from 'react' import mermaid from 'mermaid' -import mermaidAPI from 'mermaid/mermaidAPI' import { Config } from './config.model' import { getTheme } from './theme.helper' -/** - * Assign a unique ID to each mermaid svg as per requirements - * of `mermaid.render`. - */ -let id = 0 - /** * Properties for Mermaid component. */ @@ -30,7 +23,7 @@ export type MermaidProps = { /** * Config to initialize mermaid with. */ - config?: Config + config?: Config | string } /** @@ -40,26 +33,29 @@ export type MermaidProps = { * @param param1 Config. * @returns The component. */ -export const Mermaid = ({ chart, config }: MermaidProps): ReactElement<MermaidProps> => { +export const Mermaid = ({ chart, config: configSrc }: MermaidProps): ReactElement<MermaidProps> => { // Due to Docusaurus not correctly parsing client-side from server-side modules, use the provided workaround // found in the accompanying issue: https://github.com/facebook/docusaurus/issues/4268#issuecomment-783553084 /* istanbul ignore next */ if (typeof window === 'undefined') { - return <div></div> + return <div className="mermaid" data-mermaid-src={chart}>{chart}</div> } + const config: Config = useMemo(() => typeof configSrc === 'string' ? JSON.parse(configSrc) : configSrc, [configSrc]) + const html: HTMLHtmlElement = document.querySelector('html')! - // Watch for changes in theme in the HTML attribute `data-theme`. - const [theme, setTheme] = useState<mermaidAPI.Theme>(getTheme(html, config)) + const [rerender, setRerender] = useState<boolean>(false) + + const theme = useMemo(() => getTheme(html, config), [config, rerender]) useEffect(() => { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== 'attributes' || mutation.attributeName !== 'data-theme') { continue } - setTheme(getTheme(mutation.target as HTMLHtmlElement, config)) + setRerender((cur) => !cur) break } }) @@ -72,28 +68,26 @@ export const Mermaid = ({ chart, config }: MermaidProps): ReactElement<MermaidPr // Do nothing } } - }, [chart, config, theme]) + }, []) - // When theme updates, rerender the SVG. - const [svg, setSvg] = useState<string>('') useEffect(() => { - const render = () => { - mermaid.render(`mermaid-svg-${id.toString()}`, chart, (renderedSvg) => setSvg(renderedSvg)) - id++ - } - if (config) { if (config.mermaid) { mermaid.initialize({ startOnLoad: true, ...config.mermaid, theme }) } else { mermaid.initialize({ startOnLoad: true, theme }) } - render() - } else { - // Is there a better way? - setTimeout(render, 0) + document.querySelectorAll('div.mermaid[data-processed="true"]').forEach((v) => { + v.removeAttribute('data-processed') + v.innerHTML = v.getAttribute('data-mermaid-src') as string + }) + mermaid.contentLoaded() } - }, [theme, chart]) + }, [config, theme]) + + useEffect(() => { + setTimeout(() => mermaid.contentLoaded, 0) + }, [chart]) - return <div dangerouslySetInnerHTML={{ __html: svg }}></div> + return <div className="mermaid" data-mermaid-src={chart}>{chart}</div> }
src/__snapshots__/Mermaid.spec.tsx.snap+333 −8 modified@@ -1,11 +1,336 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders the output of mermaid into the div 1`] = ` -<div - dangerouslySetInnerHTML={ - Object { - "__html": "mermaid output", - } - } -/> +exports[`does not react to non-theme attribute changes of html 1`] = ` +<html + data-theme="light" +> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`does not react to non-theme attribute changes of html 2`] = ` +<html + data-theme="light" + manifest="some-value" +> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`initializes only once 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="foo" + > + foo + </div> + <div + class="mermaid" + data-mermaid-src="bar" + > + bar + </div> + </div> + </body> +</html> +`; + +exports[`re-renders mermaid theme on html data-theme attribute change 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`re-renders mermaid theme on html data-theme attribute change 2`] = ` +<html + data-theme="dark" +> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`re-renders mermaid theme on html data-theme attribute change 3`] = ` +<html + data-theme="light" +> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`renders with diagram 1`] = ` +<html> + <head /> + <body> + <div> + <svg> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </svg> + </div> + </body> +</html> +`; + +exports[`renders with diagram change 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`renders with diagram change 2`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +D-->C; +D-->B; +C-->A; +B-->A;" + > + graph TD; +D-->C; +D-->B; +C-->A; +B-->A; + </div> + </div> + </body> +</html> +`; + +exports[`renders with mermaid config 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`renders with mermaid config change 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`renders with mermaid config change 2`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`renders with string mermaid config 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="graph TD; +A-->B; +A-->C; +B-->D; +C-->D;" + > + graph TD; +A-->B; +A-->C; +B-->D; +C-->D; + </div> + </div> + </body> +</html> +`; + +exports[`renders without diagram 1`] = ` +<html> + <head /> + <body> + <div> + <div + class="mermaid" + data-mermaid-src="" + /> + </div> + </body> +</html> `;
yarn.lock+97 −20 modified@@ -1501,7 +1501,15 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.8.4": +"@babel/runtime-corejs3@^7.10.2": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.18.9.tgz#7bacecd1cb2dd694eacd32a91fcf7021c20770ae" + integrity sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A== + dependencies: + core-js-pure "^3.20.2" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== @@ -1852,6 +1860,17 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@jest/types@^27.0.6": version "27.0.6" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.6.tgz#9a992bc517e0c49f035938b8549719c2de40706b" @@ -2114,11 +2133,38 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@testing-library/dom@^7.28.1": + version "7.31.2" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a" + integrity sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.6" + lz-string "^1.4.4" + pretty-format "^26.6.2" + +"@testing-library/react@^11.1.0": + version "11.2.7" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.7.tgz#b29e2e95c6765c815786c0bc1d5aed9cb2bf7818" + integrity sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^7.28.1" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.15" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" @@ -2279,6 +2325,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== +"@types/yargs@^15.0.0": + version "15.0.14" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" + integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^16.0.0": version "16.0.4" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" @@ -2492,6 +2545,14 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz" @@ -3114,6 +3175,11 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: browserslist "^4.21.3" semver "7.0.0" +core-js-pure@^3.20.2: + version "3.24.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.24.1.tgz#8839dde5da545521bf282feb7dc6d0b425f39fd3" + integrity sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg== + cosmiconfig@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz" @@ -3563,6 +3629,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.6: + version "0.5.14" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56" + integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg== + domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -5722,6 +5793,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -6320,6 +6396,16 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-format@^27.0.0, pretty-format@^27.4.6: version "27.4.6" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" @@ -6373,33 +6459,24 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: +react-dom@^17.0.0: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" react-is@^16.8.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-shallow-renderer@^16.13.1: - version "16.14.1" - resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" - integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== - dependencies: - object-assign "^4.1.1" - react-is "^16.12.0 || ^17.0.0" - -react-test-renderer@^17.0.2: +react-is@^17.0.1: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" - integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== - dependencies: - object-assign "^4.1.1" - react-is "^17.0.2" - react-shallow-renderer "^16.13.1" - scheduler "^0.20.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react@^17.0.1: version "17.0.2"
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-rvgm-35jw-q628ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-36036ghsaADVISORY
- github.com/sjwall/mdx-mermaid/commit/f2b99386660fd13316823529c3f1314ebbcdfd2aghsax_refsource_MISCWEB
- github.com/sjwall/mdx-mermaid/security/advisories/GHSA-rvgm-35jw-q628ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.