CVE-2024-28244: Insufficient Information (NVD-CWE-noinfo)
value: KaTeX is a JavaScript library for TeX math rendering on the web. KaTeX users who render untrusted mathematical expressions could encounter malicious input using `\def` or `\newcommand` that causes a near-infinite loop, despite setting `maxExpand` to avoid such loops. KaTeX supports an option named maxExpand which aims to prevent infinitely recursive macros from consuming all available memory and/or triggering a stack overflow error. Unfortunately, support for "Unicode (sub|super)script characters" allows an attacker to bypass this limit. Each sub/superscript group instantiated a separate Parser with its own limit on macro executions, without inheriting the current count of macro executions from its parent. This has been corrected in KaTeX v0.16.10.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-25T20:15Z
Repository: https://github.com/KaTeX/KaTeX
Language: JavaScript
Files changed: src/Parser.js, test/katex-spec.js
Commit date: 2024-03-24 18:08:34 -0400
Commit message: fix: maxExpand limit with Unicode sub/superscripts

* Prevent recursive parser calls from over-expansion

* Use this.subparse instead of a recursive parser.

* Add maxExpand tests

* Lint fix

---------

Co-authored-by: Ron Kok <ronkok55@outlook.com>

Diff: diff --git a/src/Parser.js b/src/Parser.js
index 936246e7..0e002b3b 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -403,23 +403,24 @@ export default class Parser {
             } else if (uSubsAndSups[lex.text]) {
                 // A Unicode subscript or superscript character.
                 // We treat these similarly to the unicode-math package.
                 // So we render a string of Unicode (sub|super)scripts the
                 // same as a (sub|super)script of regular characters.
-                let str = uSubsAndSups[lex.text];
                 const isSub = unicodeSubRegEx.test(lex.text);
+                const subsupTokens = [];
+                subsupTokens.push(new Token(uSubsAndSups[lex.text]));
                 this.consume();
                 // Continue fetching tokens to fill out the string.
                 while (true) {
                     const token = this.fetch().text;
                     if (!(uSubsAndSups[token])) { break; }
                     if (unicodeSubRegEx.test(token) !== isSub) { break; }
+                    subsupTokens.unshift(new Token(uSubsAndSups[token]));
                     this.consume();
-                    str += uSubsAndSups[token];
                 }
                 // Now create a (sub|super)script.
-                const body = (new Parser(str, this.settings)).parse();
+                const body = this.subparse(subsupTokens);
                 if (isSub) {
                     subscript = {type: "ordgroup", mode: "math", body};
                 } else {
                     superscript = {type: "ordgroup", mode: "math", body};
                 }
diff --git a/test/katex-spec.js b/test/katex-spec.js
index bd1380f1..89cb389a 100644
--- a/test/katex-spec.js
+++ b/test/katex-spec.js
@@ -4000,10 +4000,33 @@ describe("The maxExpand setting", () => {
 
     it("should prevent exponential blowup via \\edef", () => {
         expect`\edef0{x}\edef0{00}\edef0{00}\edef0{00}\edef0{00}`.not.toParse(
             new Settings({maxExpand: 10}));
     });
+
+    const exp32 = r`
+        \def\a#1{\b{#1}\b{#1}}
+        \def\b#1{\c{#1}\c{#1}}
+        \def\c#1{\d{#1}\d{#1}}
+        \def\d#1{\e{#1}\e{#1}}
+        \def\e#1{\f{#1}\f{#1}}
+        \def\f#1{#1}
+    `;
+
+    it("should count correctly", () => {
+        const example = exp32 + r`\a{1}`;
+        const count = 1 + 2 + 4 + 8 + 16 + 32;
+        expect(example).toParse(new Settings({maxExpand: count}));
+        expect(example).not.toParse(new Settings({maxExpand: count - 1}));
+    });
+
+    it("should count correctly with Unicode sub/superscripts", () => {
+        const example = exp32 + r`\def+{\a{1}}x⁺x⁺x⁺x⁺`;
+        const count = (1 + 2 + 4 + 8 + 16 + 32) * 4 + 4;
+        expect(example).toParse(new Settings({maxExpand: count}));
+        expect(example).not.toParse(new Settings({maxExpand: count - 1}));
+    });
 });
 
 describe("The \\mathchoice function", function() {
     const cmd = r`\sum_{k = 0}^{\infty} x^k`;
 

Vulnerable code:                 let str = uSubsAndSups[lex.text];
                    str += uSubsAndSups[token];
                const body = (new Parser(str, this.settings)).parse();
Fixed code:                 const subsupTokens = [];
                subsupTokens.push(new Token(uSubsAndSups[lex.text]));
                    subsupTokens.unshift(new Token(uSubsAndSups[token]));
                const body = this.subparse(subsupTokens);

    const exp32 = r`
        \def\a#1{\b{#1}\b{#1}}
        \def\b#1{\c{#1}\c{#1}}
        \def\c#1{\d{#1}\d{#1}}
        \def\d#1{\e{#1}\e{#1}}
        \def\e#1{\f{#1}\f{#1}}
        \def\f#1{#1}
    `;

    it("should count correctly", () => {
        const example = exp32 + r`\a{1}`;
        const count = 1 + 2 + 4 + 8 + 16 + 32;
        expect(example).toParse(new Settings({maxExpand: count}));
        expect(example).not.toParse(new Settings({maxExpand: count - 1}));
    });

    it("should count correctly with Unicode sub/superscripts", () => {
        const example = exp32 + r`\def+{\a{1}}x⁺x⁺x⁺x⁺`;
        const count = (1 + 2 + 4 + 8 + 16 + 32) * 4 + 4;
        expect(example).toParse(new Settings({maxExpand: count}));
        expect(example).not.toParse(new Settings({maxExpand: count - 1}));
    });