slider

MathJax LaTeX Exploit: GitHub Users Use CSS Injection for Profile Customization

This vulnerability allows arbitrary CSS injection through the Math mode of README files, effectively permitting attackers to manipulate the style of GitHub pages. Recently, this exploit gained significant attention on X (formerly Twitter), with users showcasing their customized GitHub profiles using this method. This article details the vulnerability, how it can be exploited, and provides a historical overview of similar vulnerabilities, including insights into MathJax’s code to understand why this issue exists.

X users @cloud11665 and @Benjamin_Aster demonstrating the changes they made to their GitHub profiles, part of the large number of users showcasing their CSS-customized profiles on June 7th.

How to Achieve the Injection

GitHub employs MathJax to render math expressions in markdown content, such as README files, issue comments, and pull request comments. One of the LaTeX macros supported by MathJax is \unicode, which allows the rendering of a Unicode character with a customizable font style:

latexCopy code\unicode[myfont](x0000)

It turns out that any inline CSS can be injected within the square brackets:

latexCopy code\unicode[myfont; color: red; position: fixed; top: 0](x0000)

The CSS is injected into the style attribute of the <mtext> element that displays the Unicode character, allowing attackers to style their GitHub profile pages arbitrarily.


Diving into MathJax’s Source Code

To understand how this injection is possible, we need to examine MathJax’s source code, specifically the code handling the \unicode macro.

The Unicode Macro

javascriptCopy codeUnicodeMethods.Unicode = function (parser: TexParser, name: string) {
  let HD = parser.GetBrackets(name);
  let HDsplit = null;
  let font = null;
  if (HD) {
    if (HD.replace(/ /g, '').match(/^(\d+(\.\d*)?|\.\d+),(\d+(\.\d*)?|\.\d+)$/)) {
      HDsplit = HD.replace(/ /g, '').split(/,/);
      font = parser.GetBrackets(name);
    } else {
      font = HD;
    }
  }

  let def: EnvList = {};
  let variant = parser.stack.env.font as string;

  if (font) {
    UnicodeCache[N][2] = def.fontfamily = font.replace(/'/g, '\'');
    if (variant) {
      if (variant.match(/bold/)) {
        def.fontweight = 'bold';
      }
      if (variant.match(/italic|-mathit/)) {
        def.fontstyle = 'italic';
      }
    }
  } else if (variant) { /* ... */ }

  let node = parser.create('token', 'mtext', def, numeric(n));
  NodeUtil.setProperty(node, 'unicode', true);
}

Here’s a breakdown of the code:

  • parser.GetBrackets(name) retrieves the string within the square brackets and stores it in HD.
  • font also stores the string, depending on certain conditions applied to HD.
  • def.fontfamily holds the font value, with single quotes escaped.
  • The code then creates a parser node representing an <mtext> element, passing def as its attributes.

This process allows def.fontfamily to hold any string, including inline CSS, without proper sanitization.


The LaTeX Parser

The create method of TexParser is invoked to create nodes:

javascriptCopy codepublic create(kind : string, ...rest : any[]): MmlNode {
  return this.configuration.nodeFactory.create(kind, ...rest);
}

The nodeFactory is responsible for constructing the node representation of the Unicode macro. This leads to the AbstractMmlNode class where attributes are set:

javascriptCopy codeexport abstract class AbstractMmlNode extends AbstractNode implements MmlNode {
  constructor(factory : MmlFactory, attributes : PropertyList = {}, children : MmlNode[] = []) {
    super(factory);
    if (this.arity < 0) {
      this.childNodes = [factory.create('inferredMrow')];
      this.childNodes[0].parent = this;
    }
    this.setChildren(children);
    this.attributes = new Attributes(
      factory.getNodeClass(this.kind).defaults,
      factory.getNodeClass('math').defaults,
    );
    this.attributes.setList(attributes);
  }
}

The attributes object holds the styles, including fontfamily.


Outputting to the DOM

MathJax then converts the compiled expressions into DOM nodes. This is handled by the CHTML class, which extends CommonOutputJax:

javascriptCopy codeexport class CHTML<N, T, D> extends
CommonOutputJax<N, T, D, CHTMLWrapper<N, T, D>, CHTMLWrapperFactory<N, T, D>, CHTMLFontData, typeof CHTMLFontData> {
  protected processMath(math: MmlNode, parent: N) {
    this.factory.wrap(math).toCHTML(parent);
  }
}

The processMath method invokes toCHTML of the node wrapper class, which is CHTMLmtext for <mtext>:

javascriptCopy codeexport class CHTMLWrapper<N, T, D> extends
CommonWrapper<CHTML<N, T, D>, CHTMLWrapper<N, T, D>, CHTMLWrapperClass, CHTMLCharOptions, CHTMLDelimiterData, CHTMLFontData> {
  public toCHTML(parent: N) {
    const chtml = this.standardCHTMLnode(parent);
    for (const child of this.childNodes) {
      child.toCHTML(chtml);
    }
  }

  protected handleStyles() {
    if (!this.styles) return;
    const styles = this.styles.cssText;
    if (styles) {
      this.adaptor.setAttribute(this.chtml, 'style', styles);
    }
  }
}

The handleStyles method sets the style attribute using this.styles.cssText, which is generated from a Styles object:

javascriptCopy codepublic get cssText(): string {
  const styles = [] as string[];
  for (const name of Object.keys(this.styles)) {
    const parent = this.parentName(name);
    if (!this.styles[parent]) {
      styles.push(name + ': ' + this.styles[name] + ';');
    }
  }
  return styles.join(' ');
}

The vulnerability lies in how cssText is constructed. The fontfamily value, which includes the injected CSS, is converted to a string without proper sanitization, allowing the CSS injection to occur.


Mitigation

A safer approach involves directly manipulating the style object of the DOM element, which prevents invalid CSS values from being set:

javascriptCopy codedocument.getElementById('my-elem').style.fontFamily = "open-sans";

Attempting to inject invalid CSS through this method will be ignored by the browser.


Historical Context and Related Exploits

Past Exploits and Related Bugs

The CSS injection in MathJax, that @cloud11665 exploited in GitHub was public a year ago MathJax Issue #3129. Much before that, @SecurityMB found a similar bug (\unicode{}) but it was an XSS back then XSS in Google Colaboratory.

Bug Report: CSS Injection through Font-Family in Unicode Command (Issue #3129)

  • Reported by: @opcode86 on Nov 12, 2023
  • Summary: A user is able to inject custom CSS even if commands like \style are disabled. The style gets rendered into the style attribute of the element containing the Unicode character.
  • Steps to Reproduce:
    1. Go to any website that uses MathJax and allows the \unicode command.
    2. Enter the following code into the parser: \unicode[some-font; color:red; height: 100000px;]{x1234}.
  • Technical Details:
    • MathJax Version: 3.2.2 (latest commit: 8565f9da973238e4c9571a86a4bcb281b1d98d9b)
    • Client OS: Windows 10 Education 19045.3570
    • Browser: Chrome 119.0.6045.123

@dpvc (MathJax maintainer) acknowledged the report and suggested using the safe extension to help reduce problems caused by malevolent users. However, this extension does not handle the specific issue of CSS injection. A proposed configuration to filter the fontfamily attribute was provided:

javascriptCopy codeMathJax = {
  loader: {load: ['ui/safe']},
  startup: {
    ready() {
      MathJax.startup.defaultReady();
      const safe = MathJax.startup.document.safe;
      safe.filterAttributes.set('fontfamily', 'filterFamily');
      safe.filterMethods.filterFamily = function (safe, family) {
        return family.split(/;/)[0];
      };
    }
  }
};

This configuration filters the fontfamily attribute to remove the first ; and anything following it. The issue was promptly addressed and fixed by the MathJax team.

XSS in Google Colaboratory + Bypassing Content-Security-Policy

Michał Bentkowski discovered an XSS vulnerability in Google Colaboratory, which uses the Jupyter Notebook framework, in February 2018. The issue involved the \unicode macro of MathJax. Despite initial protection mechanisms, such as Content-Security-Policy (CSP), the vulnerability persisted due to inadequate sanitization of the \unicode macro. Bentkowski demonstrated how to bypass CSP using script gadgets in popular JS frameworks like Polymer.


The Significance of These Findings

These findings highlight the critical importance of input sanitization and security practices in web applications. The history of this vulnerability shows that even widely used and respected libraries like MathJax can have security flaws that persist over time. The ongoing discovery of such vulnerabilities and the efforts to fix them underscore the need for continuous vigilance and improvement in security practices.


Conclusion

This incident highlights the importance of sanitizing user inputs to prevent injection attacks. Libraries like DOMPurify can help sanitize HTML, MathML, and SVG. Always use library-provided mechanisms for handling user input to avoid similar vulnerabilities.

How Can Netizen Help?

Netizen ensures that security gets built-in and not bolted-on. Providing advanced solutions to protect critical IT infrastructure such as the popular “CISO-as-a-Service” wherein companies can leverage the expertise of executive-level cybersecurity professionals without having to bear the cost of employing them full time. 

We also offer compliance support, vulnerability assessments, penetration testing, and more security-related services for businesses of any size and type. 

Additionally, Netizen offers an automated and affordable assessment tool that continuously scans systems, websites, applications, and networks to uncover issues. Vulnerability data is then securely analyzed and presented through an easy-to-interpret dashboard to yield actionable risk and compliance information for audiences ranging from IT professionals to executive managers.

Netizen is an ISO 27001:2013 (Information Security Management), ISO 9001:2015, and CMMI V 2.0 Level 3 certified company. We are a proud Service-Disabled Veteran-Owned Small Business that is recognized by the U.S. Department of Labor for hiring and retention of military veterans. 

Questions or concerns? Feel free to reach out to us any time –

https://www.netizen.net/contact


Copyright © Netizen Corporation. All Rights Reserved.