Medium severityOSV Advisory· Published Aug 19, 2025· Updated Apr 15, 2026
CVE-2025-54881
CVE-2025-54881
Description
Mermaid is a JavaScript based diagramming and charting tool that uses Markdown-inspired text definitions and a renderer to create and modify complex diagrams. In the default configuration of mermaid 10.9.0-rc.1 to 11.9.0, user supplied input for sequence diagram labels is passed to innerHTML during calculation of element size, causing XSS.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mermaidnpm | >= 11.0.0-alpha.1, < 11.10.0 | 11.10.0 |
mermaidnpm | >= 10.9.0-rc.1, < 10.9.4 | 10.9.4 |
Affected products
1- Range: 0.1.0, 0.1.1, 0.2.0, …
Patches
2685516a85ec1Merge commit from fork
9 files changed · +109 −57
.changeset/tidy-weeks-play.md+7 −0 added@@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix: sanitize KATEX blocks + +Resolves CVE-2025-54881 reported by @fourcube
cypress/helpers/util.ts+3 −5 modified@@ -14,15 +14,15 @@ interface CodeObject { mermaid: CypressMermaidConfig; } -const utf8ToB64 = (str: string): string => { +export const utf8ToB64 = (str: string): string => { return Buffer.from(decodeURIComponent(encodeURIComponent(str))).toString('base64'); }; const batchId: string = 'mermaid-batch-' + (Cypress.env('useAppli') ? Date.now().toString() - : Cypress.env('CYPRESS_COMMIT') || Date.now().toString()); + : (Cypress.env('CYPRESS_COMMIT') ?? Date.now().toString())); export const mermaidUrl = ( graphStr: string | string[], @@ -61,9 +61,7 @@ export const imgSnapshotTest = ( sequence: { ...(_options.sequence ?? {}), actorFontFamily: 'courier', - noteFontFamily: _options.sequence?.noteFontFamily - ? _options.sequence.noteFontFamily - : 'courier', + noteFontFamily: _options.sequence?.noteFontFamily ?? 'courier', messageFontFamily: 'courier', }, };
cypress/integration/other/xss.spec.js+23 −1 modified@@ -1,4 +1,4 @@ -import { mermaidUrl } from '../../helpers/util.ts'; +import { imgSnapshotTest, mermaidUrl, utf8ToB64 } from '../../helpers/util.ts'; describe('XSS', () => { it('should handle xss in tags', () => { const str = @@ -141,4 +141,26 @@ describe('XSS', () => { cy.wait(1000); cy.get('#the-malware').should('not.exist'); }); + + it('should sanitize katex blocks', () => { + const str = JSON.stringify({ + code: `sequenceDiagram + participant A as Alice<img src="x" onerror="xssAttack()">$$\\text{Alice}$$ + A->>John: Hello John, how are you?`, + }); + imgSnapshotTest(utf8ToB64(str), {}, true); + cy.wait(1000); + cy.get('#the-malware').should('not.exist'); + }); + + it('should sanitize labels', () => { + const str = JSON.stringify({ + code: `erDiagram + "<img src=x onerror=xssAttack()>" ||--|| ENTITY2 : "<img src=x onerror=xssAttack()>" + `, + }); + imgSnapshotTest(utf8ToB64(str), {}, true); + cy.wait(1000); + cy.get('#the-malware').should('not.exist'); + }); });
cypress/platform/viewer.js+2 −2 modified@@ -182,7 +182,7 @@ const contentLoadedApi = async function () { for (let i = 0; i < numCodes; i++) { const { svg, bindFunctions } = await mermaid.render('newid' + i, graphObj.code[i], divs[i]); div.innerHTML = svg; - bindFunctions(div); + bindFunctions?.(div); } } else { const div = document.createElement('div'); @@ -194,7 +194,7 @@ const contentLoadedApi = async function () { const { svg, bindFunctions } = await mermaid.render('newid', graphObj.code, div); div.innerHTML = svg; console.log(div.innerHTML); - bindFunctions(div); + bindFunctions?.(div); } } };
packages/mermaid/src/dagre-wrapper/createLabel.js+8 −7 modified@@ -1,9 +1,9 @@ import { select } from 'd3'; -import { log } from '../logger.js'; import { getConfig } from '../diagram-api/diagramAPI.js'; -import { evaluate } from '../diagrams/common/common.js'; -import { decodeEntities } from '../utils.js'; +import { evaluate, sanitizeText } from '../diagrams/common/common.js'; +import { log } from '../logger.js'; import { replaceIconSubstring } from '../rendering-util/createText.js'; +import { decodeEntities } from '../utils.js'; /** * @param dom @@ -19,14 +19,14 @@ function applyStyle(dom, styleFn) { * @param {any} node * @returns {SVGForeignObjectElement} Node */ -function addHtmlLabel(node) { +function addHtmlLabel(node, config) { const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')); const div = fo.append('xhtml:div'); const label = node.label; const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel'; const span = div.append('span'); - span.html(label); + span.html(sanitizeText(label, config)); applyStyle(span, node.labelStyle); span.attr('class', labelClass); @@ -49,7 +49,8 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => { if (typeof vertexText === 'object') { vertexText = vertexText[0]; } - if (evaluate(getConfig().flowchart.htmlLabels)) { + const config = getConfig(); + if (evaluate(config.flowchart.htmlLabels)) { // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? vertexText = vertexText.replace(/\\n|\n/g, '<br />'); log.debug('vertexText' + vertexText); @@ -59,7 +60,7 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => { label, labelStyle: style.replace('fill:', 'color:'), }; - let vertexNode = addHtmlLabel(node); + let vertexNode = addHtmlLabel(node, config); // vertexNode.parentNode.removeChild(vertexNode); return vertexNode; } else {
packages/mermaid/src/diagrams/common/common.ts+16 −10 modified@@ -311,9 +311,8 @@ export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.leng * @returns Object containing \{width, height\} */ export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => { - text = await renderKatex(text, config); const divElem = document.createElement('div'); - divElem.innerHTML = text; + divElem.innerHTML = await renderKatexSanitized(text, config); divElem.id = 'katex-temp'; divElem.style.visibility = 'hidden'; divElem.style.position = 'absolute'; @@ -325,14 +324,7 @@ export const calculateMathMLDimensions = async (text: string, config: MermaidCon return dim; }; -/** - * Attempts to render and return the KaTeX portion of a string with MathML - * - * @param text - The text to test - * @param config - Configuration for Mermaid - * @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present - */ -export const renderKatex = async (text: string, config: MermaidConfig): Promise<string> => { +const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Promise<string> => { if (!hasKatex(text)) { return text; } @@ -373,6 +365,20 @@ export const renderKatex = async (text: string, config: MermaidConfig): Promise< ); }; +/** + * Attempts to render and return the KaTeX portion of a string with MathML + * + * @param text - The text to test + * @param config - Configuration for Mermaid + * @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present + */ +export const renderKatexSanitized = async ( + text: string, + config: MermaidConfig +): Promise<string> => { + return sanitizeText(await renderKatexUnsanitized(text, config), config); +}; + export default { getRows, sanitizeText,
packages/mermaid/src/diagrams/sequence/svgDraw.js+10 −6 modified@@ -1,8 +1,12 @@ -import common, { calculateMathMLDimensions, hasKatex, renderKatex } from '../common/common.js'; -import * as svgDrawCommon from '../common/svgDrawCommon.js'; -import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; import * as configApi from '../../config.js'; +import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; +import common, { + calculateMathMLDimensions, + hasKatex, + renderKatexSanitized, +} from '../common/common.js'; +import * as svgDrawCommon from '../common/svgDrawCommon.js'; export const ACTOR_TYPE_WIDTH = 18 * 2; const TOP_ACTOR_CLASS = 'actor-top'; @@ -87,13 +91,13 @@ const popupMenuToggle = function (popId) { export const drawKatex = async function (elem, textData, msgModel = null) { let textElem = elem.append('foreignObject'); - const lines = await renderKatex(textData.text, configApi.getConfig()); + const linesSanitized = await renderKatexSanitized(textData.text, configApi.getConfig()); const divElem = textElem .append('xhtml:div') .attr('style', 'width: fit-content;') .attr('xmlns', 'http://www.w3.org/1999/xhtml') - .html(lines); + .html(linesSanitized); const dim = divElem.node().getBoundingClientRect(); textElem.attr('height', Math.round(dim.height)).attr('width', Math.round(dim.width)); @@ -965,7 +969,7 @@ const _drawTextCandidateFunc = (function () { .append('div') .style('text-align', 'center') .style('vertical-align', 'middle') - .html(await renderKatex(content, configApi.getConfig())); + .html(await renderKatexSanitized(content, configApi.getConfig())); byTspan(content, s, x, y, width, height, textAttrs, conf); _setTextAttrs(text, textAttrs);
packages/mermaid/src/rendering-util/createText.ts+23 −15 modified@@ -2,9 +2,8 @@ // @ts-nocheck TODO: Fix types import { select } from 'd3'; import type { MermaidConfig } from '../config.type.js'; -import { getConfig, sanitizeText } from '../diagram-api/diagramAPI.js'; import type { SVGGroup } from '../diagram-api/types.js'; -import common, { hasKatex, renderKatex } from '../diagrams/common/common.js'; +import common, { hasKatex, renderKatexSanitized, sanitizeText } from '../diagrams/common/common.js'; import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js'; import { log } from '../logger.js'; import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js'; @@ -19,21 +18,28 @@ function applyStyle(dom, styleFn) { } } -async function addHtmlSpan(element, node, width, classes, addBackground = false) { +async function addHtmlSpan( + element, + node, + width, + classes, + addBackground = false, + // TODO: Make config mandatory + config: MermaidConfig = {} +) { const fo = element.append('foreignObject'); // This is not the final width but used in order to make sure the foreign // object in firefox gets a width at all. The final width is fetched from the div fo.attr('width', `${10 * width}px`); fo.attr('height', `${10 * width}px`); const div = fo.append('xhtml:div'); - let label = node.label; - if (node.label && hasKatex(node.label)) { - label = await renderKatex(node.label.replace(common.lineBreakRegex, '\n'), getConfig()); - } + const sanitizedLabel = hasKatex(node.label) + ? await renderKatexSanitized(node.label.replace(common.lineBreakRegex, '\n'), config) + : sanitizeText(node.label, config); const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel'; const span = div.append('span'); - span.html(label); + span.html(sanitizedLabel); applyStyle(span, node.labelStyle); span.attr('class', `${labelClass} ${classes}`); @@ -56,9 +62,6 @@ async function addHtmlSpan(element, node, width, classes, addBackground = false) bbox = div.node().getBoundingClientRect(); } - // fo.style('width', bbox.width); - // fo.style('height', bbox.height); - return fo.node(); } @@ -181,9 +184,14 @@ function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) { /** * Convert fontawesome labels into fontawesome icons by using a regex pattern * @param text - The raw string to convert + * @param config - Mermaid config * @returns string with fontawesome icons as svg if the icon is registered otherwise as i tags */ -export async function replaceIconSubstring(text: string) { +export async function replaceIconSubstring( + text: string, + // TODO: Make config mandatory + config: MermaidConfig = {} +): Promise<string> { const pendingReplacements: Promise<string>[] = []; // cspell: disable-next-line text.replace(/(fa[bklrs]?):fa-([\w-]+)/g, (fullMatch, prefix, iconName) => { @@ -193,7 +201,7 @@ export async function replaceIconSubstring(text: string) { if (await isIconAvailable(registeredIconName)) { return await getIconSVG(registeredIconName, undefined, { class: 'label-icon' }); } else { - return `<i class='${sanitizeText(fullMatch).replace(':', ' ')}'></i>`; + return `<i class='${sanitizeText(fullMatch, config).replace(':', ' ')}'></i>`; } })() ); @@ -236,7 +244,7 @@ export const createText = async ( // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? const htmlText = markdownToHTML(text, config); - const decodedReplacedText = await replaceIconSubstring(decodeEntities(htmlText)); + const decodedReplacedText = await replaceIconSubstring(decodeEntities(htmlText), config); //for Katex the text could contain escaped characters, \\relax that should be transformed to \relax const inputForKatex = text.replace(/\\\\/g, '\\'); @@ -246,7 +254,7 @@ export const createText = async ( label: hasKatex(text) ? inputForKatex : decodedReplacedText, labelStyle: style.replace('fill:', 'color:'), }; - const vertexNode = await addHtmlSpan(el, node, width, classes, addSvgBackground); + const vertexNode = await addHtmlSpan(el, node, width, classes, addSvgBackground, config); return vertexNode; } else { //sometimes the user might add br tags with 1 or more spaces in between, so we need to replace them with <br/>
packages/mermaid/src/rendering-util/rendering-elements/createLabel.js+17 −11 modified@@ -1,7 +1,12 @@ import { select } from 'd3'; -import { log } from '../../logger.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; -import common, { evaluate, renderKatex, hasKatex } from '../../diagrams/common/common.js'; +import common, { + evaluate, + hasKatex, + renderKatexSanitized, + sanitizeText, +} from '../../diagrams/common/common.js'; +import { log } from '../../logger.js'; import { decodeEntities } from '../../utils.js'; /** @@ -22,20 +27,21 @@ async function addHtmlLabel(node) { const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')); const div = fo.append('xhtml:div'); + const config = getConfig(); let label = node.label; if (node.label && hasKatex(node.label)) { - label = await renderKatex(node.label.replace(common.lineBreakRegex, '\n'), getConfig()); + label = await renderKatexSanitized(node.label.replace(common.lineBreakRegex, '\n'), config); } const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel'; - div.html( + const labelSpan = '<span class="' + - labelClass + - '" ' + - (node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive - '>' + - label + - '</span>' - ); + labelClass + + '" ' + + (node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive + '>' + + label + + '</span>'; + div.html(sanitizeText(labelSpan, config)); applyStyle(div, node.labelStyle); div.style('display', 'inline-block');
5c69e5fdb004feat(katex): optimized importing of katex
5 files changed · +93 −98
packages/mermaid/src/diagrams/common/common.ts+6 −13 modified@@ -1,6 +1,4 @@ import DOMPurify from 'dompurify'; -// @ts-ignore @types/katex does not work -import katex from 'katex'; import { MermaidConfig } from '../../config.type.js'; export const lineBreakRegex = /<br\s*\/?>/gi; @@ -219,8 +217,8 @@ export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.leng * @param config - Configuration for Mermaid * @returns Object containing \{width, height\} */ -export const calculateMathMLDimensions = (text: string, config: MermaidConfig) => { - text = renderKatex(text, config); +export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => { + text = await renderKatex(text, config); const divElem = document.createElement('div'); divElem.innerHTML = text; divElem.id = 'katex-temp'; @@ -234,22 +232,17 @@ export const calculateMathMLDimensions = (text: string, config: MermaidConfig) = return dim; }; -// export const temp = (text: string, config: MermaidConfig) => { -// return renderKatex(text, config).split(lineBreakRegex).map((text) => -// hasKatex(text) ? -// `<div style="display: flex;">${text}</div>` : -// `<div>${text}</div>`).join(''); -// } - /** * Attempts to render and return the KaTeX portion of a string with MathML * * @param text - The text to test * @param config - Configuration for Mermaid * @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present */ -export const renderKatex = (text: string, config: MermaidConfig): string => { - if (isMathMLSupported() || (!isMathMLSupported() && config.legacyMathML)) { +export const renderKatex = async (text: string, config: MermaidConfig): Promise<string> => { + if ((hasKatex(text) && isMathMLSupported()) || (!isMathMLSupported() && config.legacyMathML)) { + // @ts-ignore @types/katex does not work + const katex = (await import('katex')).default; return text .split(lineBreakRegex) .map((line) =>
packages/mermaid/src/diagrams/flowchart/flowRenderer.js+11 −11 modified@@ -28,13 +28,13 @@ export const setConf = function (cnf) { * @param _doc * @param diagObj */ -export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { +export const addVertices = async function (vert, g, svgId, root, _doc, diagObj) { const svg = !root ? select(`[id="${svgId}"]`) : root.select(`[id="${svgId}"]`); const doc = !_doc ? document : _doc; const keys = Object.keys(vert); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - keys.forEach(function (id) { + for (const id of keys) { const vertex = vert[id]; /** @@ -57,7 +57,7 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { if (evaluate(getConfig().flowchart.htmlLabels)) { // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? const node = { - label: renderKatex( + label: await renderKatex( vertexText.replace( /fa[blrs]?:fa-[\w-]+/g, (s) => `<i class='${s.replace(':', ' ')}'></i>` @@ -153,7 +153,7 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { style: styles.style, id: diagObj.db.lookUpDomId(vertex.id), }); - }); + } }; /** @@ -163,7 +163,7 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { * @param {object} g The graph object * @param diagObj */ -export const addEdges = function (edges, g, diagObj) { +export const addEdges = async function (edges, g, diagObj) { let cnt = 0; let defaultStyle; @@ -175,7 +175,7 @@ export const addEdges = function (edges, g, diagObj) { defaultLabelStyle = defaultStyles.labelStyle; } - edges.forEach(function (edge) { + for (const edge of edges) { cnt++; // Identify Link @@ -242,7 +242,7 @@ export const addEdges = function (edges, g, diagObj) { edgeData.labelType = 'html'; edgeData.label = `<span id="L-${linkId}" class="edgeLabel L-${linkNameStart}' L-${linkNameEnd}" style="${ edgeData.labelStyle - }">${renderKatex( + }">${await renderKatex( edge.text.replace( /fa[blrs]?:fa-[\w-]+/g, (s) => `<i class='${s.replace(':', ' ')}'></i>` @@ -267,7 +267,7 @@ export const addEdges = function (edges, g, diagObj) { // Add the edge to the graph g.setEdge(diagObj.db.lookUpDomId(edge.start), diagObj.db.lookUpDomId(edge.end), edgeData, cnt); - }); + } }; /** @@ -298,7 +298,7 @@ export const getClasses = function (text, diagObj) { * @param _version * @param diagObj */ -export const draw = function (text, id, _version, diagObj) { +export const draw = async function (text, id, _version, diagObj) { log.info('Drawing flowchart'); diagObj.db.clear(); const { securityLevel, flowchart: conf } = getConfig(); @@ -372,8 +372,8 @@ export const draw = function (text, id, _version, diagObj) { g.setParent(diagObj.db.lookUpDomId(subG.nodes[j]), diagObj.db.lookUpDomId(subG.id)); } } - addVertices(vert, g, id, root, doc, diagObj); - addEdges(edges, g, diagObj); + await addVertices(vert, g, id, root, doc, diagObj); + await addEdges(edges, g, diagObj); // Create the renderer const render = new Render();
packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js+10 −10 modified@@ -30,12 +30,12 @@ export const setConf = function (cnf) { * @param doc * @param diagObj */ -export const addVertices = function (vert, g, svgId, root, doc, diagObj) { +export const addVertices = async function (vert, g, svgId, root, doc, diagObj) { const svg = root.select(`[id="${svgId}"]`); const keys = Object.keys(vert); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - keys.forEach(function (id) { + for (const id of keys) { const vertex = vert[id]; /** @@ -143,7 +143,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { default: _shape = 'rect'; } - const labelText = renderKatex(vertexText, getConfig()); + const labelText = await renderKatex(vertexText, getConfig()); // Add the node g.setNode(vertex.id, { @@ -185,7 +185,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { props: vertex.props, padding: getConfig().flowchart.padding, }); - }); + } }; /** @@ -195,7 +195,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { * @param {object} g The graph object * @param diagObj */ -export const addEdges = function (edges, g, diagObj) { +export const addEdges = async function (edges, g, diagObj) { log.info('abc78 edges = ', edges); let cnt = 0; let linkIdCnt = {}; @@ -209,7 +209,7 @@ export const addEdges = function (edges, g, diagObj) { defaultLabelStyle = defaultStyles.labelStyle; } - edges.forEach(function (edge) { + for (const edge of edges) { cnt++; // Identify Link @@ -318,7 +318,7 @@ export const addEdges = function (edges, g, diagObj) { edgeData.labelpos = 'c'; } edgeData.labelType = edge.labelType; - edgeData.label = renderKatex(edge.text.replace(common.lineBreakRegex, '\n'), getConfig()); + edgeData.label = await renderKatex(edge.text.replace(common.lineBreakRegex, '\n'), getConfig()); if (edge.style === undefined) { edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; @@ -331,7 +331,7 @@ export const addEdges = function (edges, g, diagObj) { // Add the edge to the graph g.setEdge(edge.start, edge.end, edgeData, cnt); - }); + } }; /** @@ -438,8 +438,8 @@ export const draw = async function (text, id, _version, diagObj) { g.setParent(subG.nodes[j], subG.id); } } - addVertices(vert, g, id, root, doc, diagObj); - addEdges(edges, g, diagObj); + await addVertices(vert, g, id, root, doc, diagObj); + await addEdges(edges, g, diagObj); // Add custom shapes // flowChartShapes.addToRenderV2(addShape);
packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts+46 −44 modified@@ -237,7 +237,7 @@ interface NoteModel { * @param elem - The diagram to draw to. * @param noteModel - Note model options. */ -const drawNote = function (elem: any, noteModel: NoteModel) { +const drawNote = async function (elem: any, noteModel: NoteModel) { bounds.bumpVerticalPos(conf.boxMargin); noteModel.height = conf.boxMargin; noteModel.starty = bounds.getVerticalPos(); @@ -263,7 +263,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) { textObj.textMargin = conf.noteMargin; textObj.valign = 'center'; - const textElem = hasKatex(textObj.text) ? drawKatex(g, textObj) : drawText(g, textObj); + const textElem = hasKatex(textObj.text) ? await drawKatex(g, textObj) : drawText(g, textObj); const textHeight = Math.round( textElem @@ -311,13 +311,13 @@ const actorFont = (cnf) => { * @param msgModel - The model containing fields describing a message * @returns `lineStartY` - The Y coordinate at which the message line starts */ -function boundMessage(_diagram, msgModel): number { +async function boundMessage(_diagram, msgModel): number { bounds.bumpVerticalPos(10); const { startx, stopx, message } = msgModel; const lines = common.splitBreaks(message).length; const isKatexMsg = hasKatex(message); const textDims = isKatexMsg - ? calculateMathMLDimensions(message, configApi.getConfig()) + ? await calculateMathMLDimensions(message, configApi.getConfig()) : utils.calculateTextDimensions(message, messageFont(conf)); if (!isKatexMsg) { @@ -365,7 +365,7 @@ function boundMessage(_diagram, msgModel): number { * @param lineStartY - The Y coordinate at which the message line starts * @param diagObj - The diagram object. */ -const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { +const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textObj = svgDrawCommon.getTextObj(); @@ -384,7 +384,7 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di textObj.tspan = false; hasKatex(textObj.text) - ? drawKatex(diagram, textObj, { startx, stopx, starty: lineStartY }) + ? await drawKatex(diagram, textObj, { startx, stopx, starty: lineStartY }) : drawText(diagram, textObj); const textWidth = textDims.width; @@ -485,7 +485,7 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di } }; -export const drawActors = function ( +export const drawActors = async function ( diagram, actors, actorKeys, @@ -539,7 +539,7 @@ export const drawActors = function ( actor.y = bounds.getVerticalPos(); // Draw the box with the attached line - const height = svgDraw.drawActor(diagram, actor, conf, isFooter); + const height = await svgDraw.drawActor(diagram, actor, conf, isFooter); maxHeight = common.getMax(maxHeight, height); bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height); @@ -648,7 +648,7 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop * @param _version - Mermaid version from package.json * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram */ -export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) { +export const draw = async function (_text: string, id: string, _version: string, diagObj: Diagram) { const { securityLevel, sequence } = configApi.getConfig(); conf = sequence; diagObj.db.clear(); @@ -679,8 +679,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO const title = diagObj.db.getDiagramTitle(); const hasBoxes = diagObj.db.hasAtLeastOneBox(); const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle(); - const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj); - conf.height = calculateActorMargins(actors, maxMessageWidthPerActor, boxes); + const maxMessageWidthPerActor = await getMaxMessageWidthPerActor(actors, messages, diagObj); + conf.height = await calculateActorMargins(actors, maxMessageWidthPerActor, boxes); svgDraw.insertComputerIcon(diagram); svgDraw.insertDatabaseIcon(diagram); @@ -693,8 +693,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO } } - drawActors(diagram, actors, actorKeys, 0, conf, messages, false); - const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj); + await drawActors(diagram, actors, actorKeys, 0, conf, messages, false); + const loopWidths = await calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj); // The arrow head definition is attached to the svg once svgDraw.insertArrowHead(diagram); @@ -727,14 +727,14 @@ export const draw = function (_text: string, id: string, _version: string, diagO let sequenceIndex = 1; let sequenceIndexStep = 1; const messagesToDraw = []; - messages.forEach(function (msg) { + for (const msg of messages) { let loopModel, noteModel, msgModel; switch (msg.type) { case diagObj.db.LINETYPE.NOTE: bounds.resetVerticalPos(); noteModel = msg.noteModel; - drawNote(diagram, noteModel); + await drawNote(diagram, noteModel); break; case diagObj.db.LINETYPE.ACTIVE_START: bounds.newActivation(msg, diagram, actors); @@ -753,7 +753,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.LOOP_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'loop', conf); + await svgDraw.drawLoop(diagram, loopModel, 'loop', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -779,7 +779,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.OPT_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'opt', conf); + await svgDraw.drawLoop(diagram, loopModel, 'opt', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -803,7 +803,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.ALT_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'alt', conf); + await svgDraw.drawLoop(diagram, loopModel, 'alt', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -829,7 +829,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.PAR_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'par', conf); + await svgDraw.drawLoop(diagram, loopModel, 'par', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -862,7 +862,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.CRITICAL_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'critical', conf); + await svgDraw.drawLoop(diagram, loopModel, 'critical', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -877,7 +877,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.BREAK_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'break', conf); + await svgDraw.drawLoop(diagram, loopModel, 'break', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -889,7 +889,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO msgModel.starty = bounds.getVerticalPos(); msgModel.sequenceIndex = sequenceIndex; msgModel.sequenceVisible = diagObj.db.showSequenceNumbers(); - const lineStartY = boundMessage(diagram, msgModel); + const lineStartY = await boundMessage(diagram, msgModel); messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY }); bounds.models.addMessage(msgModel); } catch (e) { @@ -912,28 +912,30 @@ export const draw = function (_text: string, id: string, _version: string, diagO ) { sequenceIndex = sequenceIndex + sequenceIndexStep; } - }); + } - messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj)); + for (const e of messagesToDraw) { + await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj); + } if (conf.mirrorActors) { // Draw actors below diagram bounds.bumpVerticalPos(conf.boxMargin * 2); - drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true); + await drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true); bounds.bumpVerticalPos(conf.boxMargin); fixLifeLineHeights(diagram, bounds.getVerticalPos()); } - bounds.models.boxes.forEach(function (box) { + for (const box of bounds.models.boxes) { box.height = bounds.getVerticalPos() - box.y; bounds.insert(box.x, box.y, box.x + box.width, box.height); box.startx = box.x; box.starty = box.y; box.stopx = box.startx + box.width; box.stopy = box.starty + box.height; box.stroke = 'rgb(0,0,0, 0.5)'; - svgDraw.drawBox(diagram, box, conf); - }); + await svgDraw.drawBox(diagram, box, conf); + } if (hasBoxes) { bounds.bumpVerticalPos(conf.boxMargin); @@ -1004,14 +1006,14 @@ export const draw = function (_text: string, id: string, _version: string, diagO * @param diagObj - The diagram object. * @returns The max message width of each actor. */ -function getMaxMessageWidthPerActor( +async function getMaxMessageWidthPerActor( actors: { [id: string]: any }, messages: any[], diagObj: Diagram -): { [id: string]: number } { +): Promise<{ [id: string]: number }> { const maxMessageWidthPerActor = {}; - messages.forEach(function (msg) { + for (const msg of messages) { if (actors[msg.to] && actors[msg.from]) { const actor = actors[msg.to]; @@ -1033,7 +1035,7 @@ function getMaxMessageWidthPerActor( ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont) : msg.message; const messageDimensions = hasKatex(wrappedMessage) - ? calculateMathMLDimensions(msg.message, configApi.getConfig()) + ? await calculateMathMLDimensions(msg.message, configApi.getConfig()) : utils.calculateTextDimensions(wrappedMessage, textFont); const messageWidth = messageDimensions.width + 2 * conf.wrapPadding; @@ -1099,7 +1101,7 @@ function getMaxMessageWidthPerActor( } } } - }); + } log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor); return maxMessageWidthPerActor; @@ -1130,13 +1132,13 @@ const getRequiredPopupWidth = function (actor) { * @param actorToMessageWidth - A map of actor key → max message width it holds * @param boxes - The boxes around the actors if any */ -function calculateActorMargins( +async function calculateActorMargins( actors: { [id: string]: any }, - actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>, + actorToMessageWidth: Awaited<ReturnType<typeof getMaxMessageWidthPerActor>>, boxes ) { let maxHeight = 0; - Object.keys(actors).forEach((prop) => { + for (const prop of Object.keys(actors)) { const actor = actors[prop]; if (actor.wrap) { actor.description = utils.wrapLabel( @@ -1146,7 +1148,7 @@ function calculateActorMargins( ); } const actDims = hasKatex(actor.description) - ? calculateMathMLDimensions(actor.description, configApi.getConfig()) + ? await calculateMathMLDimensions(actor.description, configApi.getConfig()) : utils.calculateTextDimensions(actor.description, actorFont(conf)); actor.width = actor.wrap @@ -1155,7 +1157,7 @@ function calculateActorMargins( actor.height = actor.wrap ? common.getMax(actDims.height, conf.height) : conf.height; maxHeight = common.getMax(maxHeight, actor.height); - }); + } for (const actorKey in actorToMessageWidth) { const actor = actors[actorKey]; @@ -1206,13 +1208,13 @@ function calculateActorMargins( return common.getMax(maxHeight, conf.height); } -const buildNoteModel = function (msg, actors, diagObj) { +const buildNoteModel = async function (msg, actors, diagObj) { const startx = actors[msg.from].x; const stopx = actors[msg.to].x; const shouldWrap = msg.wrap && msg.message; let textDimensions: { width: number; height: number; lineHeight?: number } = hasKatex(msg.message) - ? calculateMathMLDimensions(msg.message, configApi.getConfig()) + ? await calculateMathMLDimensions(msg.message, configApi.getConfig()) : utils.calculateTextDimensions( shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message, noteFont(conf) @@ -1338,12 +1340,12 @@ const buildMessageModel = function (msg, actors, diagObj) { }; }; -const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagObj) { +const calculateLoopBounds = async function (messages, actors, _maxWidthPerActor, diagObj) { const loops = {}; const stack = []; let current, noteModel, msgModel; - messages.forEach(function (msg) { + for (const msg of messages) { msg.id = utils.random({ length: 10 }); switch (msg.type) { case diagObj.db.LINETYPE.LOOP_START: @@ -1406,7 +1408,7 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO } const isNote = msg.placement !== undefined; if (isNote) { - noteModel = buildNoteModel(msg, actors, diagObj); + noteModel = await buildNoteModel(msg, actors, diagObj); msg.noteModel = noteModel; stack.forEach((stk) => { current = stk; @@ -1445,7 +1447,7 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO }); } } - }); + } bounds.activations = []; log.debug('Loop type widths:', loops); return loops;
packages/mermaid/src/diagrams/sequence/svgDraw.js+20 −20 modified@@ -119,9 +119,9 @@ const popupMenuDownFunc = function (popupId) { } }; -export const drawKatex = function (elem, textData, msgModel = null) { +export const drawKatex = async function (elem, textData, msgModel = null) { let textElem = elem.append('foreignObject'); - const lines = renderKatex(textData.text, configApi.getConfig()); + const lines = await renderKatex(textData.text, configApi.getConfig()); const divElem = textElem .append('xhtml:div') @@ -353,7 +353,7 @@ export const fixLifeLineHeights = (diagram, bounds) => { * @param {any} conf - DrawText implementation discriminator object * @param {boolean} isFooter - If the actor is the footer one */ -const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { +const drawActorTypeParticipant = async function (elem, actor, conf, isFooter) { const center = actor.x + actor.width / 2; const centerY = actor.y + 5; @@ -407,7 +407,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { } } - _drawTextCandidateFunc(conf, hasKatex(actor.description))( + await _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, g, rect.x, @@ -428,10 +428,9 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { return height; }; -const drawActorTypeActor = function (elem, actor, conf, isFooter) { +const drawActorTypeActor = async function (elem, actor, conf, isFooter) { const center = actor.x + actor.width / 2; const centerY = actor.y + 80; - if (!isFooter) { actorCnt++; elem @@ -496,7 +495,7 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) { const bounds = actElem.node().getBBox(); actor.height = bounds.height; - _drawTextCandidateFunc(conf, hasKatex(actor.description))( + await _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, actElem, rect.x, @@ -510,21 +509,21 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) { return actor.height; }; -export const drawActor = function (elem, actor, conf, isFooter) { +export const drawActor = async function (elem, actor, conf, isFooter) { switch (actor.type) { case 'actor': - return drawActorTypeActor(elem, actor, conf, isFooter); + return await drawActorTypeActor(elem, actor, conf, isFooter); case 'participant': - return drawActorTypeParticipant(elem, actor, conf, isFooter); + return await drawActorTypeParticipant(elem, actor, conf, isFooter); } }; -export const drawBox = function (elem, box, conf) { +export const drawBox = async function (elem, box, conf) { const boxplustextGroup = elem.append('g'); const g = boxplustextGroup; drawBackgroundRect(g, box); if (box.name) { - _drawTextCandidateFunc(conf)( + await _drawTextCandidateFunc(conf)( box.name, g, box.x, @@ -571,7 +570,7 @@ export const drawActivation = function (elem, bounds, verticalPos, conf, actorAc * @param {any} conf - Diagram configuration * @returns {any} */ -export const drawLoop = function (elem, loopModel, labelText, conf) { +export const drawLoop = async function (elem, loopModel, labelText, conf) { const { boxMargin, boxTextMargin, @@ -633,10 +632,10 @@ export const drawLoop = function (elem, loopModel, labelText, conf) { txt.fontWeight = fontWeight; txt.wrap = true; - let textElem = hasKatex(txt.text) ? drawKatex(g, txt, loopModel) : drawText(g, txt); + let textElem = hasKatex(txt.text) ? await drawKatex(g, txt, loopModel) : drawText(g, txt); if (loopModel.sectionTitles !== undefined) { - loopModel.sectionTitles.forEach(function (item, idx) { + for (const [idx, item] of Object.entries(loopModel.sectionTitles)) { if (item.message) { txt.text = item.message; txt.x = loopModel.startx + (loopModel.stopx - loopModel.startx) / 2; @@ -652,7 +651,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) { if (hasKatex(txt.text)) { loopModel.starty = loopModel.sections[idx].y; - drawKatex(g, txt, loopModel); + await drawKatex(g, txt, loopModel); } else { drawText(g, txt); } @@ -663,7 +662,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) { ); loopModel.sections[idx].height += sectionHeight - (boxMargin + boxTextMargin); } - }); + } } loopModel.height = Math.round(loopModel.stopy - loopModel.starty); @@ -951,9 +950,10 @@ const _drawTextCandidateFunc = (function () { * @param textAttrs * @param conf */ - function byKatex(content, g, x, y, width, height, textAttrs, conf) { + async function byKatex(content, g, x, y, width, height, textAttrs, conf) { // TODO duplicate render calls, optimize - const dim = calculateMathMLDimensions(content, configApi.getConfig()); + + const dim = await calculateMathMLDimensions(content, configApi.getConfig()); const s = g.append('switch'); const f = s .append('foreignObject') @@ -968,7 +968,7 @@ const _drawTextCandidateFunc = (function () { .append('div') .style('text-align', 'center') .style('vertical-align', 'middle') - .html(renderKatex(content, configApi.getConfig())); + .html(await renderKatex(content, configApi.getConfig())); byTspan(content, s, x, y, width, height, textAttrs, conf); _setTextAttrs(text, textAttrs);
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
5- github.com/advisories/GHSA-7rqq-prvp-x9jhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54881ghsaADVISORY
- github.com/mermaid-js/mermaid/commit/5c69e5fdb004a6d0a2abe97e23d26e223a059832nvdWEB
- github.com/mermaid-js/mermaid/commit/685516a85ec1df64cefd4fd15f26533be87d458envdWEB
- github.com/mermaid-js/mermaid/security/advisories/GHSA-7rqq-prvp-x9jhnvdWEB
News mentions
0No linked articles in our index yet.