From 05d28f38b22ecd5a66f1658d8931a8cc05f32795 Mon Sep 17 00:00:00 2001 From: Brahim Hamichan Date: Wed, 2 Sep 2026 00:46:52 +0100 Subject: [PATCH 1/2] fix(web): render data URI images in chat markdown Assistant messages embedding images as data: URIs rendered terminal 'Image unavailable' placeholders (#9094): react-markdown's default URL transform empties every data: URL, and the sanitize schema dropped the protocol on img src. Allow data:image/ sources through both layers so inline data images render; non-image data URIs stay blocked. Fixed with Claude Fable 5 on Claude Code. --- apps/web/src/components/ChatMarkdown.tsx | 7 +++++-- .../ChatMarkdown.workspace-images.test.tsx | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 75127ea124e8..0753a1adde72 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -387,10 +387,13 @@ const CHAT_MARKDOWN_SANITIZE_SCHEMA = { protocols: { ...defaultSchema.protocols, href: [...(defaultSchema.protocols?.href ?? []), "file", "t3-citation"], - src: [...(defaultSchema.protocols?.src ?? []), "file"], + src: [...(defaultSchema.protocols?.src ?? []), "file", "data"], }, } satisfies Parameters[0]; +/** Inline image payloads are safe on both href and src; an img never runs scripts. */ +const INLINE_DATA_IMAGE_PATTERN = /^data:image\//i; + const CHAT_MARKDOWN_REMARK_PLUGINS = [ remarkGfm, remarkGithubAlerts, @@ -2106,7 +2109,7 @@ function ChatMarkdown({ }, [inlineCodeFileLinkMetaByText, markdownFileLinkMetaByHref]); const markdownUrlTransform = useCallback((href: string) => { if (parseAssistantCitationHref(href)) return href; - if (isWindowsDrivePathHref(href)) return href; + if (isWindowsDrivePathHref(href) || INLINE_DATA_IMAGE_PATTERN.test(href)) return href; return rewriteMarkdownFileUriHref(href) ?? defaultUrlTransform(href); }, []); // Re-emit highlighted content as markdown so copying out of the rendered diff --git a/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx b/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx index 39be0eedafe2..37c1ed7cd1ec 100644 --- a/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx +++ b/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx @@ -316,4 +316,25 @@ describe("ChatMarkdown workspace images", () => { expect(html).toContain("max-h-[30rem]"); expect(html).not.toContain("Image unavailable"); }); + + it.each([ + ["markdown", (uri: string) => `![inline data](${uri})`], + ["raw HTML", (uri: string) => `inline data`], + ])("keeps %s data URI images directly loadable", (_variant, source) => { + const dataUri = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + const html = render(source(dataUri)); + + expect(testState.resources).toEqual([]); + expect(html).toContain(`src="${dataUri}"`); + expect(html).not.toContain("Image unavailable"); + }); + + it("blocks non-image data URIs instead of passing them to a raw image", () => { + const html = render("![payload](data:text/html;base64,PHNjcmlwdC8+)"); + + expect(testState.resources).toEqual([]); + expect(html).toContain("Image unavailable"); + expect(html).not.toContain("data:text/html"); + }); }); From 29c43e7a03b3606f52f902d2699e05bb2737f1ed Mon Sep 17 00:00:00 2001 From: Brahim Hamichan Date: Wed, 2 Sep 2026 03:46:03 +0100 Subject: [PATCH 2/2] test(web): pin raw HTML non-image data URIs to the blocked fallback Written by Claude Fable 5 on Claude Code. --- .../src/components/ChatMarkdown.workspace-images.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx b/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx index 37c1ed7cd1ec..1dd7850f29f4 100644 --- a/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx +++ b/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx @@ -330,8 +330,11 @@ describe("ChatMarkdown workspace images", () => { expect(html).not.toContain("Image unavailable"); }); - it("blocks non-image data URIs instead of passing them to a raw image", () => { - const html = render("![payload](data:text/html;base64,PHNjcmlwdC8+)"); + it.each([ + ["markdown", "![payload](data:text/html;base64,PHNjcmlwdC8+)"], + ["raw HTML", 'payload'], + ])("blocks non-image %s data URIs instead of passing them to a raw image", (_variant, source) => { + const html = render(source); expect(testState.resources).toEqual([]); expect(html).toContain("Image unavailable");