From b07715fc08db9297987e3fe6ace45ac17eed24e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 3 Jul 2026 09:31:28 +0200 Subject: [PATCH] Harden the streaming code-block guard and narrow provider serialization --- src/lib/components/CodeBlock.svelte | 10 ++--- src/lib/server/api/types.ts | 4 +- src/lib/server/api/utils/serializeModel.ts | 10 ++++- src/lib/utils/markedLight.spec.ts | 48 ++++++++++++++++++++++ src/lib/utils/markedLight.ts | 18 ++++++++ 5 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 src/lib/utils/markedLight.spec.ts 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,