Merge branch 'advisory-fix-1' of https://github.com/mermaid-js/mermaid-ghsa-7rqq-prvp-x9jh into advisory-fix-1

* 'advisory-fix-1' of https://github.com/mermaid-js/mermaid-ghsa-7rqq-prvp-x9jh:
  fix: Sanitize Katex
This commit is contained in:
Sidharth Vinod
2025-08-05 22:37:06 +05:30
4 changed files with 48 additions and 29 deletions

View File

@@ -311,9 +311,8 @@ export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.leng
* @returns Object containing \{width, height\} * @returns Object containing \{width, height\}
*/ */
export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => { export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => {
text = await renderKatex(text, config);
const divElem = document.createElement('div'); const divElem = document.createElement('div');
divElem.innerHTML = text; divElem.innerHTML = await renderKatexSanitized(text, config);
divElem.id = 'katex-temp'; divElem.id = 'katex-temp';
divElem.style.visibility = 'hidden'; divElem.style.visibility = 'hidden';
divElem.style.position = 'absolute'; divElem.style.position = 'absolute';
@@ -325,14 +324,7 @@ export const calculateMathMLDimensions = async (text: string, config: MermaidCon
return dim; return dim;
}; };
/** const renderKatexUnsanitized = async (text: string, config: MermaidConfig): Promise<string> => {
* 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> => {
if (!hasKatex(text)) { if (!hasKatex(text)) {
return 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 { export default {
getRows, getRows,
sanitizeText, sanitizeText,

View File

@@ -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 { sanitizeUrl } from '@braintree/sanitize-url';
import * as configApi from '../../config.js'; 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; export const ACTOR_TYPE_WIDTH = 18 * 2;
const TOP_ACTOR_CLASS = 'actor-top'; const TOP_ACTOR_CLASS = 'actor-top';
@@ -87,13 +91,13 @@ const popupMenuToggle = function (popId) {
export const drawKatex = async function (elem, textData, msgModel = null) { export const drawKatex = async function (elem, textData, msgModel = null) {
let textElem = elem.append('foreignObject'); let textElem = elem.append('foreignObject');
const lines = await renderKatex(textData.text, configApi.getConfig()); const linesSanitized = await renderKatexSanitized(textData.text, configApi.getConfig());
const divElem = textElem const divElem = textElem
.append('xhtml:div') .append('xhtml:div')
.attr('style', 'width: fit-content;') .attr('style', 'width: fit-content;')
.attr('xmlns', 'http://www.w3.org/1999/xhtml') .attr('xmlns', 'http://www.w3.org/1999/xhtml')
.html(lines); .html(linesSanitized);
const dim = divElem.node().getBoundingClientRect(); const dim = divElem.node().getBoundingClientRect();
textElem.attr('height', Math.round(dim.height)).attr('width', Math.round(dim.width)); textElem.attr('height', Math.round(dim.height)).attr('width', Math.round(dim.width));
@@ -965,7 +969,7 @@ const _drawTextCandidateFunc = (function () {
.append('div') .append('div')
.style('text-align', 'center') .style('text-align', 'center')
.style('vertical-align', 'middle') .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); byTspan(content, s, x, y, width, height, textAttrs, conf);
_setTextAttrs(text, textAttrs); _setTextAttrs(text, textAttrs);

View File

@@ -4,7 +4,7 @@ import { select } from 'd3';
import type { MermaidConfig } from '../config.type.js'; import type { MermaidConfig } from '../config.type.js';
import { getConfig, sanitizeText } from '../diagram-api/diagramAPI.js'; import { getConfig, sanitizeText } from '../diagram-api/diagramAPI.js';
import type { SVGGroup } from '../diagram-api/types.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 type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js';
import { log } from '../logger.js'; import { log } from '../logger.js';
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.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'); const div = fo.append('xhtml:div');
let label = node.label; let label = node.label;
if (node.label && hasKatex(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 labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
const span = div.append('span'); const span = div.append('span');

View File

@@ -1,7 +1,12 @@
import { select } from 'd3'; import { select } from 'd3';
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.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'; 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 fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
const div = fo.append('xhtml:div'); const div = fo.append('xhtml:div');
const config = getConfig();
let label = node.label; let label = node.label;
if (node.label && hasKatex(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'; const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
div.html( const labelSpan =
'<span class="' + '<span class="' +
labelClass + labelClass +
'" ' + '" ' +
(node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive (node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive
'>' + '>' +
label + label +
'</span>' '</span>';
); div.html(sanitizeText(labelSpan, config));
applyStyle(div, node.labelStyle); applyStyle(div, node.labelStyle);
div.style('display', 'inline-block'); div.style('display', 'inline-block');