diff --git a/bun.lock b/bun.lock index eedb7c1fbd..66f54a9f7b 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "mux", @@ -58,6 +57,7 @@ "parse-duration": "^2.1.4", "posthog-node": "^5.17.0", "rehype-harden": "^1.1.5", + "rehype-sanitize": "^6.0.0", "shescape": "^2.1.6", "source-map-support": "^0.5.21", "streamdown": "1.6.10", @@ -2285,6 +2285,8 @@ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], @@ -3145,6 +3147,8 @@ "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], + "release-zalgo": ["release-zalgo@1.0.0", "", { "dependencies": { "es6-error": "^4.0.1" } }, "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA=="], "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="], diff --git a/package.json b/package.json index 6facaa6f6d..db2f3245b2 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "parse-duration": "^2.1.4", "posthog-node": "^5.17.0", "rehype-harden": "^1.1.5", + "rehype-sanitize": "^6.0.0", "shescape": "^2.1.6", "source-map-support": "^0.5.21", "streamdown": "1.6.10", diff --git a/src/browser/components/Messages/MarkdownCore.tsx b/src/browser/components/Messages/MarkdownCore.tsx index 7e870251f2..a7e8205caa 100644 --- a/src/browser/components/Messages/MarkdownCore.tsx +++ b/src/browser/components/Messages/MarkdownCore.tsx @@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import rehypeRaw from "rehype-raw"; +import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import { harden } from "rehype-harden"; import "katex/dist/katex.min.css"; import { normalizeMarkdown } from "./MarkdownStyles"; @@ -30,10 +31,57 @@ const REMARK_PLUGINS: Pluggable[] = [ [remarkMath, { singleDollarTextMath: false }], ]; +// Schema for rehype-sanitize that allows safe HTML elements. +// Extends the default schema to support KaTeX math and collapsible sections. +const sanitizeSchema = { + ...defaultSchema, + tagNames: [ + ...(defaultSchema.tagNames ?? []), + // KaTeX MathML elements + "math", + "mrow", + "mi", + "mo", + "mn", + "msup", + "msub", + "mfrac", + "munder", + "mover", + "mtable", + "mtr", + "mtd", + "mspace", + "mtext", + "semantics", + "annotation", + "munderover", + "msqrt", + "mroot", + "mpadded", + "mphantom", + "menclose", + // Collapsible sections (GitHub-style) + "details", + "summary", + ], + attributes: { + ...defaultSchema.attributes, + // KaTeX uses style for coloring and positioning + span: [...(defaultSchema.attributes?.span ?? []), "style"], + // MathML elements need various attributes + math: ["xmlns", "display"], + annotation: ["encoding"], + // Allow class on all elements for styling + "*": [...(defaultSchema.attributes?.["*"] ?? []), "className", "class"], + }, +}; + const REHYPE_PLUGINS: Pluggable[] = [ rehypeRaw, // Parse HTML elements first + [rehypeSanitize, sanitizeSchema], // Sanitize HTML to prevent XSS (strips dangerous elements/attributes) [ - harden, // Sanitize after parsing raw HTML to prevent XSS + harden, // Additional URL filtering for links and images { allowedImagePrefixes: ["*"], allowedLinkPrefixes: ["*"],