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());
};