-
+
{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 = `
+
+
+
+
+
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
',
+ },
});
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");