VYPR
Medium severity6.3OSV Advisory· Published Apr 1, 2025· Updated Apr 15, 2026

CVE-2025-29049

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.

PackageAffected versionsPatched versions
mathlivenpm
< 0.104.00.104.0

Affected products

1

Patches

1
abc26056fd5e

fix: harden `\htmlData` against XSS

https://github.com/arnog/mathliveArno GourdolJan 18, 2025via ghsa
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

4

News mentions

0

No linked articles in our index yet.