fix(web): render HTML in markdown previews (#1707)

This commit is contained in:
Carlos Florêncio
2026-07-15 19:34:34 +01:00
committed by GitHub
parent af6756bbbc
commit f2e3d68340
3 changed files with 63 additions and 2 deletions
@@ -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({
<div ref={scrollRef} className="flex-1 overflow-auto p-6">
<div ref={rootRef} className="markdown-body max-w-3xl" tabIndex={commentsEnabled ? 0 : -1}>
<PreviewCommentContext.Provider value={previewCommentContextValue}>
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownPreviewComponents}>
<ReactMarkdown
remarkPlugins={remarkPlugins}
rehypePlugins={[rehypeRaw, [rehypeSanitize, defaultSchema]]}
components={markdownPreviewComponents}
>
{content}
</ReactMarkdown>
</PreviewCommentContext.Provider>
@@ -23,6 +23,21 @@ console.log("hello");
\`\`\`
`;
const HTML_MARKDOWN_CONTENT = `<div align="center">
<a href="https://github.com/kdlbs/kandev">
<img width="120" height="80" src="./logo.svg" alt="Kandev logo" onerror="alert('xss')">
</a>
<br>
<h1>Embedded HTML</h1>
<p>Rendered from a README.</p>
<details open>
<summary>More information</summary>
<p>Collapsible README content.</p>
</details>
<a href="javascript:alert('xss')">Unsafe link</a>
<script>window.markdownXss = true</script>
</div>`;
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,
@@ -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<div align="center"><p>Embedded HTML body</p></div>',
},
});
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");