mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-19 04:04:28 +01:00
Merge commit from fork
fix: Sanitize icons and icon labels
This commit is contained in:
7
.changeset/good-weeks-tickle.md
Normal file
7
.changeset/good-weeks-tickle.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: sanitize icon labels and icon SVGs
|
||||||
|
|
||||||
|
Resolves CVE-2025-54880 reported by @fourcube
|
||||||
@@ -142,6 +142,17 @@ describe('XSS', () => {
|
|||||||
cy.get('#the-malware').should('not.exist');
|
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 "<img src=x onerror=\\"xssAttack()\\">" [Database] in api`,
|
||||||
|
});
|
||||||
|
imgSnapshotTest(utf8ToB64(str), {}, true);
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('#the-malware').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
it('should sanitize katex blocks', () => {
|
it('should sanitize katex blocks', () => {
|
||||||
const str = JSON.stringify({
|
const str = JSON.stringify({
|
||||||
code: `sequenceDiagram
|
code: `sequenceDiagram
|
||||||
|
|||||||
@@ -1118,7 +1118,7 @@ end
|
|||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`flowchart TB
|
`flowchart TB
|
||||||
A(["Start"]) --> n1["Untitled Node"]
|
A(["Start"]) --> n1["Untitled Node"]
|
||||||
A --> n2["Untitled Node"]
|
A --> n2["Untitled Node"]
|
||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -1127,7 +1127,7 @@ end
|
|||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`flowchart BT
|
`flowchart BT
|
||||||
n2["Untitled Node"] --> n1["Diamond"]
|
n2["Untitled Node"] --> n1["Diamond"]
|
||||||
n1@{ shape: diam}
|
n1@{ shape: diam}
|
||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -1138,7 +1138,7 @@ end
|
|||||||
n2["Untitled Node"] --> n1["Rounded Rectangle"]
|
n2["Untitled Node"] --> n1["Rounded Rectangle"]
|
||||||
n3["Untitled Node"] --> n1
|
n3["Untitled Node"] --> n1
|
||||||
n1@{ shape: rounded}
|
n1@{ shape: rounded}
|
||||||
n3@{ shape: rect}
|
n3@{ shape: rect}
|
||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,14 +20,14 @@
|
|||||||
width: 800
|
width: 800
|
||||||
nodeAlignment: left
|
nodeAlignment: left
|
||||||
---
|
---
|
||||||
sankey
|
sankey
|
||||||
a,b,8
|
a,b,8
|
||||||
b,c,8
|
b,c,8
|
||||||
c,d,8
|
c,d,8
|
||||||
d,e,8
|
d,e,8
|
||||||
|
|
||||||
x,c,4
|
x,c,4
|
||||||
c,y,4
|
c,y,4
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<h2>Energy flow</h2>
|
<h2>Energy flow</h2>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
|
|||||||
import { createText } from '../../rendering-util/createText.js';
|
import { createText } from '../../rendering-util/createText.js';
|
||||||
import { getIconSVG } from '../../rendering-util/icons.js';
|
import { getIconSVG } from '../../rendering-util/icons.js';
|
||||||
import type { D3Element } from '../../types.js';
|
import type { D3Element } from '../../types.js';
|
||||||
|
import { sanitizeText } from '../common/common.js';
|
||||||
import type { ArchitectureDB } from './architectureDb.js';
|
import type { ArchitectureDB } from './architectureDb.js';
|
||||||
import { architectureIcons } from './architectureIcons.js';
|
import { architectureIcons } from './architectureIcons.js';
|
||||||
import {
|
import {
|
||||||
@@ -271,6 +272,7 @@ export const drawServices = async function (
|
|||||||
elem: D3Element,
|
elem: D3Element,
|
||||||
services: ArchitectureService[]
|
services: ArchitectureService[]
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
const config = getConfig();
|
||||||
for (const service of services) {
|
for (const service of services) {
|
||||||
const serviceElem = elem.append('g');
|
const serviceElem = elem.append('g');
|
||||||
const iconSize = db.getConfigField('iconSize');
|
const iconSize = db.getConfigField('iconSize');
|
||||||
@@ -285,7 +287,7 @@ export const drawServices = async function (
|
|||||||
width: iconSize * 1.5,
|
width: iconSize * 1.5,
|
||||||
classes: 'architecture-service-label',
|
classes: 'architecture-service-label',
|
||||||
},
|
},
|
||||||
getConfig()
|
config
|
||||||
);
|
);
|
||||||
|
|
||||||
textElem
|
textElem
|
||||||
@@ -320,7 +322,7 @@ export const drawServices = async function (
|
|||||||
.attr('class', 'node-icon-text')
|
.attr('class', 'node-icon-text')
|
||||||
.attr('style', `height: ${iconSize}px;`)
|
.attr('style', `height: ${iconSize}px;`)
|
||||||
.append('div')
|
.append('div')
|
||||||
.html(service.iconText);
|
.html(sanitizeText(service.iconText, config));
|
||||||
const fontSize =
|
const fontSize =
|
||||||
parseInt(
|
parseInt(
|
||||||
window
|
window
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Drawing a pie chart is really simple in mermaid.
|
|||||||
|
|
||||||
**Note:**
|
**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.
|
> **Negative values are not allowed** and will result in an error.
|
||||||
|
|
||||||
[pie] [showData] (OPTIONAL)
|
[pie] [showData] (OPTIONAL)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { replaceIconSubstring } from './createText.js';
|
import { sanitizeText } from '../diagram-api/diagramAPI.js';
|
||||||
import mermaid from '../mermaid.js';
|
import mermaid from '../mermaid.js';
|
||||||
|
import { replaceIconSubstring } from './createText.js';
|
||||||
|
|
||||||
describe('replaceIconSubstring', () => {
|
describe('replaceIconSubstring', () => {
|
||||||
it('converts FontAwesome icon notations to HTML tags', async () => {
|
it('converts FontAwesome icon notations to HTML tags', async () => {
|
||||||
@@ -56,7 +57,7 @@ describe('replaceIconSubstring', () => {
|
|||||||
]);
|
]);
|
||||||
const input = 'Icons galore: fa:fa-bell';
|
const input = 'Icons galore: fa:fa-bell';
|
||||||
const output = await replaceIconSubstring(input);
|
const output = await replaceIconSubstring(input);
|
||||||
const expected = staticBellIconPack.icons.bell.body;
|
const expected = sanitizeText(staticBellIconPack.icons.bell.body);
|
||||||
expect(output).toContain(expected);
|
expect(output).toContain(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { log } from '../logger.js';
|
|
||||||
import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/types';
|
import type { ExtendedIconifyIcon, IconifyIcon, IconifyJSON } from '@iconify/types';
|
||||||
import type { IconifyIconCustomisations } from '@iconify/utils';
|
import type { IconifyIconCustomisations } from '@iconify/utils';
|
||||||
import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } 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 {
|
interface AsyncIconLoader {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -100,5 +102,5 @@ export const getIconSVG = async (
|
|||||||
...renderData.attributes,
|
...renderData.attributes,
|
||||||
...extraAttributes,
|
...extraAttributes,
|
||||||
});
|
});
|
||||||
return svg;
|
return sanitizeText(svg, getConfig());
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user