From 9d685178d215f76be4d5e8fe47c64dd915274738 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 5 Aug 2025 20:44:03 +0530 Subject: [PATCH] fix: Sanitize Katex --- .../mermaid/src/diagrams/common/common.ts | 26 ++++++++++------- .../mermaid/src/diagrams/sequence/svgDraw.js | 16 +++++++---- .../mermaid/src/rendering-util/createText.ts | 7 +++-- .../rendering-elements/createLabel.js | 28 +++++++++++-------- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 00c9b8313..3b3fdd41e 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -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 => { +const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Promise => { 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 => { + return sanitizeText(await renderKatexUnsanitized(text, config), config); +}; + export default { getRows, sanitizeText, diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 04ccd8a84..18fd2d034 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -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); diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 6dad6b214..65129aa8a 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -4,7 +4,7 @@ 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 } 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'; @@ -29,7 +29,10 @@ async function addHtmlSpan(element, node, width, classes, addBackground = false) 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()); + label = await renderKatexSanitized( + node.label.replace(common.lineBreakRegex, '\n'), + getConfig() + ); } const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel'; const span = div.append('span'); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js b/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js index 482dbb9f1..de6f0403d 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/createLabel.js @@ -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 = '' + - label + - '' - ); + labelClass + + '" ' + + (node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive + '>' + + label + + ''; + div.html(sanitizeText(labelSpan, config)); applyStyle(div, node.labelStyle); div.style('display', 'inline-block');