CVE-2025-29049
Description
Cross Site Scripting vulnerability in arnog MathLive Versions v0.103.0 and before (fixed in 0.104.0) allows an attacker to execute arbitrary code via the MathLive function.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
MathLive <=0.103.0 lacks escaping in `\htmlData` and other HTML attributes, enabling stored or reflected XSS via crafted LaTeX expressions.
Vulnerability
Description
CVE-2025-29049 is a Cross-Site Scripting (XSS) vulnerability in the MathLive library (versions 0.103.0 and earlier), which provides web components for rendering and editing mathematical expressions. The root cause is a lack of proper escaping or sanitization of HTML attributes when processing certain LaTeX commands, specifically the \htmlData command. The library’s Box class did not sanitize attribute names or values before inserting them into the DOM, allowing an attacker to inject arbitrary HTML attributes or event handlers [1][4].
Exploitation
An attacker can exploit this by providing a crafted LaTeX expression containing \htmlData with malicious payloads, such as \htmlData{><img/onerror=alert(1) src>}{}. When this expression is rendered by the MathLive component, the unsanitized content is injected directly into the HTML without escaping, causing the browser to execute the attacker’s JavaScript [4]. The attack does not require authentication or a special network position if the application renders untrusted user-supplied LaTeX (e.g., in a comment system, document editor, or educational platform).
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim’s browser session. This can lead to session hijacking, credential theft, defacement, or redirection to malicious sites. The vulnerability is rated Medium (CVSS 6.3) because it requires the application to render untrusted LaTeX, but no other special privileges are needed [3].
Mitigation
The issue has been fixed in MathLive version 0.104.0 by introducing a sanitizeAttributeName and sanitizeAttributeValue functions that properly escape attribute names and values before they are inserted into the HTML [1]. Users and applications relying on MathLive should upgrade to 0.104.0 or later. There is no announced workaround for earlier versions other than avoiding the rendering of untrusted LaTeX input [2][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mathlivenpm | < 0.104.0 | 0.104.0 |
Affected products
1Patches
1abc26056fd5efix: harden `\htmlData` against XSS
3 files changed · +93 −42
src/core/box.ts+56 −10 modified@@ -423,15 +423,17 @@ export class Box implements BoxInterface { .filter((x, e, a) => x.length > 0 && a.indexOf(x) === e) .join(' '); - if (classList.length > 0) props += ` class="${classList}"`; + if (classList.length > 0) + props += ` class=${sanitizeAttributeValue(`"${classList}"`)}`; // // 3.2 Id // - if (this.id) props += ` data-atom-id=${this.id}`; + if (this.id) props += ` data-atom-id=${sanitizeAttributeValue(this.id)}`; // A (HTML5) CSS id may not contain a space - if (this.cssId) props += ` id="${this.cssId.replace(/ /g, '-')}" `; + if (this.cssId) + props += ` id=${sanitizeAttributeValue(`"${this.cssId.replace(/ /g, '-')}"`)}`; // // 3.3 Attributes @@ -440,7 +442,10 @@ export class Box implements BoxInterface { props += ' ' + Object.keys(this.attributes) - .map((x) => `${x}="${this.attributes![x]}"`) + .map( + (x) => + `${sanitizeAttributeName(x)}=${sanitizeAttributeValue(`"${this.attributes![x]}"`)}` + ) .join(' '); } @@ -449,11 +454,12 @@ export class Box implements BoxInterface { for (const entry of entries) { const matched = entry.match(/([^=]+)=(.+$)/); if (matched) { - const key = matched[1].trim().replace(/ /g, '-'); - if (key) props += ` data-${key}="${matched[2]}" `; + const key = sanitizeAttributeName(matched[1]); + const value = sanitizeAttributeValue(matched[2]); + if (key) props += ` ${key}=${value}`; } else { - const key = entry.trim().replace(/ /g, '-'); - if (key) props += ` data-${key} `; + const key = sanitizeAttributeName(entry); + if (key) props += ` ${key} `; } } } @@ -488,10 +494,12 @@ export class Box implements BoxInterface { if (key) styleString += `${key}:${matched[2]};`; } } - if (styleString) props += ` style="${styleString}"`; + if (styleString) + props += ` style=${sanitizeAttributeValue(`"${styleString}"`)};`; } - if (styles.length > 0) props += ` style="${styles.join(';')}"`; + if (styles.length > 0) + props += ` style=${sanitizeAttributeValue(`"${styles.join(';')}"`)}`; // // 4. Tag markup @@ -736,3 +744,41 @@ function horizontalLayout(box: Box, fontName: FontName): void { box.maxFontSize = maxFontSize; } } + +function sanitizeAttributeName(attribute: string): string { + attribute = attribute.trim().replace(/ /g, '-'); + + /** + * https://w3c.github.io/html-reference/syntax.html#syntax-attributes + * + * > Attribute Names must consist of one or more characters + * other than the space characters, U+0000 NULL, + * '"', "'", ">", "/", "=", the control characters, + * and any characters that are not defined by Unicode. + */ + const invalidAttributeNameRegex = /[\s"'>/=\x00-\x1f]/; + if (invalidAttributeNameRegex.test(attribute)) + throw new Error(`Invalid attribute name: ${attribute}`); + + return attribute; +} + +function sanitizeAttributeValue(value: string): string { + value = value.trim(); + + if (value.startsWith('"') && value.endsWith('"')) { + // Must not contain any `"` + if (value.slice(1, -1).match(/"/)) + throw new Error(`Invalid attribute value: ${value}`); + } else if (value.startsWith("'") && value.endsWith("'")) { + // Must not contain any `'` + if (value.slice(1, -1).match(/'/)) + throw new Error(`Invalid attribute value: ${value}`); + } else { + // Must not contain any literal space characters, `"`, `'`, `=`, `>`, `<` or backtick characters + if (value.match(/[\s"'`=><`]/)) + throw new Error(`Invalid attribute value: ${value}`); + } + + return value; +}
src/editor-mathfield/render.ts+36 −31 modified@@ -102,41 +102,46 @@ export function contentMarkup( mathfield: _Mathfield, renderOptions?: { forHighlighting?: boolean; interactive?: boolean } ): string { - // - // 1. Update selection state and blinking cursor (caret) - // - const { model } = mathfield; - model.root.caret = undefined; - model.root.isSelected = false; - model.root.containsCaret = true; - for (const atom of model.atoms) { - atom.caret = undefined; - atom.isSelected = false; - atom.containsCaret = false; - } - if (model.selectionIsCollapsed) { - const atom = model.at(model.position); - atom.caret = mathfield.model.mode; - let ancestor = atom.parent; - while (ancestor) { - ancestor.containsCaret = true; - ancestor = ancestor.parent; + try { + // + // 1. Update selection state and blinking cursor (caret) + // + const { model } = mathfield; + model.root.caret = undefined; + model.root.isSelected = false; + model.root.containsCaret = true; + for (const atom of model.atoms) { + atom.caret = undefined; + atom.isSelected = false; + atom.containsCaret = false; + } + if (model.selectionIsCollapsed) { + const atom = model.at(model.position); + atom.caret = mathfield.model.mode; + let ancestor = atom.parent; + while (ancestor) { + ancestor.containsCaret = true; + ancestor = ancestor.parent; + } + } else { + const atoms = model.getAtoms(model.selection, { includeChildren: true }); + for (const atom of atoms) atom.isSelected = true; } - } else { - const atoms = model.getAtoms(model.selection, { includeChildren: true }); - for (const atom of atoms) atom.isSelected = true; - } - // - // 2. Render a box representation of the mathfield content - // - const box = makeBox(mathfield, renderOptions); + // + // 2. Render a box representation of the mathfield content + // + const box = makeBox(mathfield, renderOptions); - // - // 3. Generate markup - // + // + // 3. Generate markup + // - return box.toMarkup(); + return box.toMarkup(); + } catch (e) { + console.error(e); + return '<span class="ML__latex" translate="no" aria-hidden="true">💣</span>'; + } } /**
src/public/mathfield-element.ts+1 −1 modified@@ -789,7 +789,7 @@ export class MathfieldElement extends HTMLElement implements Mathfield { } /** - * Support for [Trusted Type](https://w3c.github.io/webappsec-trusted-types/dist/spec/). + * Support for [Trusted Type](https://www.w3.org/TR/trusted-types/). * * This optional function will be called before a string of HTML is * injected in the DOM, allowing that string to be sanitized
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.