diff --git a/.changeset/good-weeks-tickle.md b/.changeset/good-weeks-tickle.md new file mode 100644 index 000000000..97c0c3660 --- /dev/null +++ b/.changeset/good-weeks-tickle.md @@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix: sanitize icon labels and icon SVGs + +Resolves CVE-2025-54880 reported by @fourcube diff --git a/cypress/integration/other/xss.spec.js b/cypress/integration/other/xss.spec.js index 603e75f5d..35a493850 100644 --- a/cypress/integration/other/xss.spec.js +++ b/cypress/integration/other/xss.spec.js @@ -142,6 +142,17 @@ describe('XSS', () => { cy.get('#the-malware').should('not.exist'); }); + it('should sanitize icon labels in architecture diagrams', () => { + const str = JSON.stringify({ + code: `architecture-beta + group api(cloud)[API] + service db "" [Database] in api`, + }); + imgSnapshotTest(utf8ToB64(str), {}, true); + cy.wait(1000); + cy.get('#the-malware').should('not.exist'); + }); + it('should sanitize katex blocks', () => { const str = JSON.stringify({ code: `sequenceDiagram diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index 6756c433d..8c6cde57a 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -1118,7 +1118,7 @@ end imgSnapshotTest( `flowchart TB A(["Start"]) --> n1["Untitled Node"] - A --> n2["Untitled Node"] + A --> n2["Untitled Node"] `, {} ); @@ -1127,7 +1127,7 @@ end imgSnapshotTest( `flowchart BT n2["Untitled Node"] --> n1["Diamond"] - n1@{ shape: diam} + n1@{ shape: diam} `, {} ); @@ -1138,7 +1138,7 @@ end n2["Untitled Node"] --> n1["Rounded Rectangle"] n3["Untitled Node"] --> n1 n1@{ shape: rounded} - n3@{ shape: rect} + n3@{ shape: rect} `, {} ); diff --git a/demos/sankey.html b/demos/sankey.html index 11bb541c2..92e0f587d 100644 --- a/demos/sankey.html +++ b/demos/sankey.html @@ -20,14 +20,14 @@ width: 800 nodeAlignment: left --- - sankey + sankey a,b,8 b,c,8 c,d,8 d,e,8 - + x,c,4 - c,y,4 + c,y,4

Energy flow

diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index f384defd8..6e470caa2 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -3,6 +3,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; import { createText } from '../../rendering-util/createText.js'; import { getIconSVG } from '../../rendering-util/icons.js'; import type { D3Element } from '../../types.js'; +import { sanitizeText } from '../common/common.js'; import type { ArchitectureDB } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import { @@ -271,6 +272,7 @@ export const drawServices = async function ( elem: D3Element, services: ArchitectureService[] ): Promise { + const config = getConfig(); for (const service of services) { const serviceElem = elem.append('g'); const iconSize = db.getConfigField('iconSize'); @@ -285,7 +287,7 @@ export const drawServices = async function ( width: iconSize * 1.5, classes: 'architecture-service-label', }, - getConfig() + config ); textElem @@ -320,7 +322,7 @@ export const drawServices = async function ( .attr('class', 'node-icon-text') .attr('style', `height: ${iconSize}px;`) .append('div') - .html(service.iconText); + .html(sanitizeText(service.iconText, config)); const fontSize = parseInt( window diff --git a/packages/mermaid/src/docs/syntax/pie.md b/packages/mermaid/src/docs/syntax/pie.md index 416119b5b..a311c87ef 100644 --- a/packages/mermaid/src/docs/syntax/pie.md +++ b/packages/mermaid/src/docs/syntax/pie.md @@ -26,7 +26,7 @@ Drawing a pie chart is really simple in mermaid. **Note:** -> Pie chart values must be **positive numbers greater than zero**. +> Pie chart values must be **positive numbers greater than zero**. > **Negative values are not allowed** and will result in an error. [pie] [showData] (OPTIONAL) diff --git a/packages/mermaid/src/rendering-util/createText.spec.ts b/packages/mermaid/src/rendering-util/createText.spec.ts index e2e13ef7d..dd7bc00b6 100644 --- a/packages/mermaid/src/rendering-util/createText.spec.ts +++ b/packages/mermaid/src/rendering-util/createText.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { replaceIconSubstring } from './createText.js'; +import { sanitizeText } from '../diagram-api/diagramAPI.js'; import mermaid from '../mermaid.js'; +import { replaceIconSubstring } from './createText.js'; describe('replaceIconSubstring', () => { it('converts FontAwesome icon notations to HTML tags', async () => { @@ -56,7 +57,7 @@ describe('replaceIconSubstring', () => { ]); const input = 'Icons galore: fa:fa-bell'; const output = await replaceIconSubstring(input); - const expected = staticBellIconPack.icons.bell.body; + const expected = sanitizeText(staticBellIconPack.icons.bell.body); expect(output).toContain(expected); }); }); diff --git a/packages/mermaid/src/rendering-util/icons.ts b/packages/mermaid/src/rendering-util/icons.ts index 50b1bbeb9..65f35bc62 100644 --- a/packages/mermaid/src/rendering-util/icons.ts +++ b/packages/mermaid/src/rendering-util/icons.ts @@ -1,7 +1,9 @@ -import { log } from '../logger.js'; import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/types'; import type { IconifyIconCustomisations } from '@iconify/utils'; import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } from '@iconify/utils'; +import { getConfig } from '../config.js'; +import { sanitizeText } from '../diagrams/common/common.js'; +import { log } from '../logger.js'; interface AsyncIconLoader { name: string; @@ -100,5 +102,5 @@ export const getIconSVG = async ( ...renderData.attributes, ...extraAttributes, }); - return svg; + return sanitizeText(svg, getConfig()); };