CVE-2024-28244 (published 2024-03-25T20:15Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/KaTeX/KaTeX
commit_details: repo_url: https://github.com/KaTeX/KaTeX
author: Erik Demaine
author_date: 2024-03-24 17:59:46-04:00
msg: 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>
repository: repo_url: https://github.com/KaTeX/KaTeX
repo_name: KaTeX/KaTeX
description: Fast math typesetting for the web.
repo_language: JavaScript
owner: KaTeX
file_changes: filename: Parser.js
change_type: ModificationType.MODIFY
diff: @@ -405,19 +405,20 @@ export default class Parser {
                 // 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 {

programming_language: TypeScript
filename: katex-spec.js
change_type: ModificationType.MODIFY
diff: @@ -4002,6 +4002,29 @@ describe("The maxExpand setting", () => {
         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() {

programming_language: TypeScript