CVE-2024-28246 (JavaScript, published 2024-03-25T20:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/KaTeX/KaTeX
Files changed:
docs/options.md
src/Settings.js
src/utils.js
test/__snapshots__/katex-spec.js.snap
test/katex-spec.js
Commit message: 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


Vulnerable code:
            context.protocol = utils.protocolFromUrl(context.url);
 * protocol (and thus is relative).
export const protocolFromUrl = function(url: string): string {
    const protocol = /^\s*([^\\/#]*?)(?::|&#0*58|&#x0*3a)/i.exec(url);
    return (protocol != null ? protocol[1] : "_relative");
exports[`href and url commands should not allow explicitly disallow protocols 1`] = `
    it("should not allow explicitly disallow protocols", () => {

Fixed code:
    where `protocol` is a lowercased string like `"http"` or `"https"`
    that appears before a colon, or `"_relative"` for relative URLs.
            const protocol = utils.protocolFromUrl(context.url);
            if (protocol == null) {
                return false;
            }
            context.protocol = protocol;
 * protocol (and thus is relative), or `null` if URL has invalid protocol
 * (so should be outright rejected).
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();
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`] = `
    it("should not allow explicitly disallowed protocols", () => {

    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");
    });