From f2e3d68340fed2904d427ee8b1e2f7c329e118df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Wed, 15 Jul 2026 19:34:34 +0100 Subject: [PATCH] fix(web): render HTML in markdown previews (#1707) --- .../task/markdown-preview-content.tsx | 8 ++- .../e2e/tests/chat/markdown-preview.spec.ts | 51 +++++++++++++++++++ .../e2e/tests/task/mobile-file-viewer.spec.ts | 6 ++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/web/components/task/markdown-preview-content.tsx b/apps/web/components/task/markdown-preview-content.tsx index d338e5f683..b5ced6ccad 100644 --- a/apps/web/components/task/markdown-preview-content.tsx +++ b/apps/web/components/task/markdown-preview-content.tsx @@ -14,6 +14,8 @@ import { } from "react"; import { createPortal } from "react-dom"; import ReactMarkdown, { type ExtraProps, type Components } from "react-markdown"; +import rehypeRaw from "rehype-raw"; +import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import { Button } from "@kandev/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { IconCode, IconMessagePlus } from "@tabler/icons-react"; @@ -355,7 +357,11 @@ export const MarkdownPreviewContent = memo(function MarkdownPreviewContent({
- + {content} diff --git a/apps/web/e2e/tests/chat/markdown-preview.spec.ts b/apps/web/e2e/tests/chat/markdown-preview.spec.ts index 779c6d6fa0..a7b0d705b3 100644 --- a/apps/web/e2e/tests/chat/markdown-preview.spec.ts +++ b/apps/web/e2e/tests/chat/markdown-preview.spec.ts @@ -23,6 +23,21 @@ console.log("hello"); \`\`\` `; +const HTML_MARKDOWN_CONTENT = `
+ + Kandev logo + +
+

Embedded HTML

+

Rendered from a README.

+
+ More information +

Collapsible README content.

+
+ Unsafe link + +
`; + async function seedTaskWithSession( testPage: Page, apiClient: ApiClient, @@ -147,6 +162,42 @@ test.describe("Markdown preview", () => { await expect(preview).not.toBeVisible({ timeout: 5_000 }); }); + test("renders safe embedded HTML and strips executable content", async ({ + testPage, + apiClient, + seedData, + backend, + }) => { + const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo"); + fs.writeFileSync(path.join(repoDir, "html-readme.md"), HTML_MARKDOWN_CONTENT); + + const { session } = await seedTaskWithSession( + testPage, + apiClient, + seedData, + "Markdown Embedded HTML Test", + ); + await openFileInPreview(testPage, session, "html-readme.md"); + + const preview = testPage.getByTestId("markdown-preview"); + const centeredContent = preview.locator('div[align="center"]'); + await expect(centeredContent).toBeVisible(); + await expect(centeredContent.getByRole("heading", { name: "Embedded HTML" })).toBeVisible(); + + const image = centeredContent.getByRole("img", { name: "Kandev logo" }); + await expect(image).toHaveAttribute("src", "./logo.svg"); + await expect(image).toHaveAttribute("width", "120"); + await expect(image).not.toHaveAttribute("onerror"); + await expect(centeredContent.locator("p").first()).toHaveAttribute("data-md-source-start", "7"); + await expect(centeredContent.locator("details")).toContainText("Collapsible README content."); + await expect(centeredContent.locator("summary")).toHaveText("More information"); + await expect(centeredContent.locator("a", { hasText: "Unsafe link" })).not.toHaveAttribute( + "href", + ); + await expect(preview.locator("script")).toHaveCount(0); + expect(await testPage.evaluate(() => "markdownXss" in window)).toBe(false); + }); + test("markdown preview persists across page refresh", async ({ testPage, apiClient, diff --git a/apps/web/e2e/tests/task/mobile-file-viewer.spec.ts b/apps/web/e2e/tests/task/mobile-file-viewer.spec.ts index 2a12c30692..081065e89d 100644 --- a/apps/web/e2e/tests/task/mobile-file-viewer.spec.ts +++ b/apps/web/e2e/tests/task/mobile-file-viewer.spec.ts @@ -263,7 +263,10 @@ test.describe("Mobile file viewer panel", () => { seedData, backend, taskTitle: "Mobile FV Markdown", - options: { extension: "md", content: "# Heading\n\nBody text" }, + options: { + extension: "md", + content: '# Heading\n\n

Embedded HTML body

', + }, }); await testPage.getByRole("button", { name: "Files" }).tap(); @@ -277,6 +280,7 @@ test.describe("Mobile file viewer panel", () => { await viewer.getByTestId("markdown-preview-toggle").tap(); await expect(viewer.getByTestId("markdown-preview")).toBeVisible(); + await expect(viewer.locator('div[align="center"]')).toContainText("Embedded HTML body"); await selectMarkdownPreviewText(viewer.getByTestId("markdown-preview").locator("p").first()); const commentButton = testPage.getByTestId("markdown-preview-comment-button");