diff --git a/src/lib/components/CodeBlock.svelte b/src/lib/components/CodeBlock.svelte
index 5856fa6467e..a18bfd49696 100644
--- a/src/lib/components/CodeBlock.svelte
+++ b/src/lib/components/CodeBlock.svelte
@@ -1,6 +1,7 @@
")).toBe(false);
+ expect(isTrustedHighlighterHtml('
')).toBe(false);
+ expect(isTrustedHighlighterHtml("")).toBe(false);
+ expect(isTrustedHighlighterHtml("a < b")).toBe(false);
+ });
+
+ it("rejects malformed closing spans", () => {
+ expect(isTrustedHighlighterHtml("")).toBe(false);
+ expect(isTrustedHighlighterHtml('')).toBe(false);
+ });
+});
diff --git a/src/lib/utils/markedLight.ts b/src/lib/utils/markedLight.ts
index 6e02fe6b2b6..ccb355f8fa4 100644
--- a/src/lib/utils/markedLight.ts
+++ b/src/lib/utils/markedLight.ts
@@ -26,6 +26,24 @@ export type BlockToken = {
tokens: Token[];
};
+// Matches any `<` that opens something other than the exact markup our
+// highlighter can emit: ``, ``, or `` (double
+// quotes, single space, no other attributes). Anything else - including a
+// span with any additional or differently quoted attribute - is not
+// highlighter output and must be sanitized.
+const NON_HIGHLIGHTER_TAG = /<(?!\/span>|span(?: class="[^"]*")?>)/i;
+
+/**
+ * True when `html` contains only markup the markdown highlighter itself can
+ * produce (escaped text plus hljs-style span/class wrappers). Used to decide
+ * when a streaming code block may skip DOMPurify: the check enforces the
+ * highlighter's output alphabet directly instead of relying on the implicit
+ * contract that highlightCode() never emits attributes.
+ */
+export function isTrustedHighlighterHtml(html: string): boolean {
+ return !NON_HIGHLIGHTER_TAG.test(html);
+}
+
export function escapeHTML(content: string) {
return content.replace(
/[<>&"']/g,