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

KaTeX is a JavaScript library for TeX math rendering on the web. Code that uses KaTeX's `trust` option, specifically that provides a function to blacklist certain URL protocols, can be fooled by URLs in malicious inputs that use uppercase characters in the protocol. In particular, this can allow for malicious input to generate `javascript:` links in the output, even if the `trust` function tries to forbid this protocol via `trust: (context) => context.protocol !== 'javascript'`. Upgrade to KaTeX v0.16.10 to remove this vulnerability.


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 18:03:28-04:00
msg: fix: force protocol to be lowercase for better protocol filtering

* fix: force protocol to be lowercase for better protocol filtering

See https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
"Although schemes are case-insensitive, the canonical form is lowercase"

* Update snapshots

* Rewrite to use new URL if available, and reject weird/invalid protocols

* Fix prefix pattern, ? handling, add comments and tests

* Remove new URL to simplify code

* Lint fixes
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: options.md
change_type: ModificationType.MODIFY
diff: @@ -50,6 +50,8 @@ You can provide an object of options as the last argument to [`katex.render` and
 - `trust`: `boolean` or `function` (default: `false`). If `false` (do not trust input), prevent any commands like `\includegraphics` that could enable adverse behavior, rendering them instead in `errorColor`. If `true` (trust input), allow all such commands. Provide a custom function `handler(context)` to customize behavior depending on the context (command, arguments e.g. a URL, etc.).  A list of possible contexts:
 
   - `{command: "\\url", url, protocol}`
+    where `protocol` is a lowercased string like `"http"` or `"https"`
+    that appears before a colon, or `"_relative"` for relative URLs.
   - `{command: "\\href", url, protocol}`
   - `{command: "\\includegraphics", url, protocol}`
   - `{command: "\\htmlClass", class}`

programming_language: Markdown
filename: Settings.js
change_type: ModificationType.MODIFY
diff: @@ -346,7 +346,11 @@ export default class Settings {
      */
     isTrusted(context: AnyTrustContext): boolean {
         if (context.url && !context.protocol) {
-            context.protocol = utils.protocolFromUrl(context.url);
+            const protocol = utils.protocolFromUrl(context.url);
+            if (protocol == null) {
+                return false;
+            }
+            context.protocol = protocol;
         }
         const trust = typeof this.trust === "function"
             ? this.trust(context)

programming_language: TypeScript
filename: utils.js
change_type: ModificationType.MODIFY
diff: @@ -93,11 +93,30 @@ export const assert = function<T>(value: ?T): T {
 
 /**
  * Return the protocol of a URL, or "_relative" if the URL does not specify a
- * protocol (and thus is relative).
+ * protocol (and thus is relative), or `null` if URL has invalid protocol
+ * (so should be outright rejected).
  */
-export const protocolFromUrl = function(url: string): string {
-    const protocol = /^\s*([^\\/#]*?)(?::|&#0*58|&#x0*3a)/i.exec(url);
-    return (protocol != null ? protocol[1] : "_relative");
+export const protocolFromUrl = function(url: string): string | null {
+    // Check for possible leading protocol.
+    // https://url.spec.whatwg.org/#url-parsing strips leading whitespace
+    // (U+20) or C0 control (U+00-U+1F) characters.
+    // eslint-disable-next-line no-control-regex
+    const protocol = /^[\x00-\x20]*([^\\/#?]*?)(:|&#0*58|&#x0*3a|&colon)/i
+    .exec(url);
+    if (!protocol) {
+        return "_relative";
+    }
+    // Reject weird colons
+    if (protocol[2] !== ":") {
+        return null;
+    }
+    // Reject invalid characters in scheme according to
+    // https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
+    if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(protocol[1])) {
+        return null;
+    }
+    // Lowercase the protocol
+    return protocol[1].toLowerCase();
 };
 
 export default {

programming_language: TypeScript
filename: katex-spec.js.snap
change_type: ModificationType.MODIFY
diff: @@ -1468,7 +1468,50 @@ exports[`href and url commands should not affect spacing around 1`] = `
 ]
 `;
 
-exports[`href and url commands should not allow explicitly disallow protocols 1`] = `
+exports[`href and url commands should not allow explicitly disallowed protocols 1`] = `
+[
+  {
+    "type": "color",
+    "body": [
+      {
+        "type": "text",
+        "body": [
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "\\\\"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "h"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "r"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "e"
+          },
+          {
+            "type": "textord",
+            "mode": "text",
+            "text": "f"
+          }
+        ],
+        "mode": "math"
+      }
+    ],
+    "color": "#cc0000",
+    "mode": "math"
+  }
+]
+`;
+
+exports[`href and url commands should not allow explicitly uppercased disallowed protocols 1`] = `
 [
   {
     "type": "color",

programming_language: TypeScript
filename: katex-spec.js
change_type: ModificationType.MODIFY
diff: @@ -3005,13 +3005,46 @@ describe("href and url commands", function() {
         expect(parsed).toMatchSnapshot();
     });
 
-    it("should not allow explicitly disallow protocols", () => {
+    it("should not allow explicitly disallowed protocols", () => {
         const parsed = getParsed(
             "\\href{javascript:alert('x')}{foo}",
             new Settings({trust: context => context.protocol !== "javascript"}),
         );
         expect(parsed).toMatchSnapshot();
     });
+
+    it("should not allow explicitly uppercased disallowed protocols", () => {
+        const parsed = getParsed(
+            "\\href{JavaScript:alert('x')}{foo}",
+            new Settings({trust: context => context.protocol !== "javascript"}),
+        );
+        expect(parsed).toMatchSnapshot();
+    });
+
+    function getProtocolViaTrust(url) {
+        let protocol;
+        getParsed(`\\url{${url}}`, new Settings({
+            trust: context => protocol = context.protocol,
+        }));
+        return protocol;
+    }
+
+    it("should get protocols correctly", () => {
+        expect(getProtocolViaTrust("foo")).toBe("_relative");
+        expect(getProtocolViaTrust("Foo:")).toBe("foo");
+        expect(getProtocolViaTrust("Foo:bar")).toBe("foo");
+        expect(getProtocolViaTrust("JavaScript:")).toBe("javascript");
+        expect(getProtocolViaTrust("JavaScript:code")).toBe("javascript");
+        expect(getProtocolViaTrust("!:")).toBeUndefined();
+        expect(getProtocolViaTrust("foo&colon;")).toBeUndefined();
+        expect(getProtocolViaTrust("?query=string&colon=")).toBe("_relative");
+        expect(getProtocolViaTrust("#query=string&colon=")).toBe("_relative");
+        expect(getProtocolViaTrust("dir/file&colon")).toBe("_relative");
+        expect(getProtocolViaTrust("//foo")).toBe("_relative");
+        expect(getProtocolViaTrust("://foo")).toBeUndefined();
+        expect(getProtocolViaTrust("  \t http://")).toBe("http");
+        expect(getProtocolViaTrust("  \t http://foo")).toBe("http");
+    });
 });
 
 describe("A raw text parser", function() {

programming_language: TypeScript