fix: flatten remote markdown images

This commit is contained in:
Ayaan Zaidi
2026-03-07 19:16:11 +05:30
committed by Ayaan Zaidi
parent 53a7e3b6e5
commit 4bf902de58
6 changed files with 133 additions and 19 deletions

View File

@@ -30,11 +30,10 @@ describe("toSanitizedMarkdownHtml", () => {
expect(html).toContain("console.log(1)");
});
it("preserves img tags with src and alt from markdown images (#15437)", () => {
it("flattens remote markdown images into alt text", () => {
const html = toSanitizedMarkdownHtml("![Alt text](https://example.com/image.png)");
expect(html).toContain("<img");
expect(html).toContain('src="https://example.com/image.png"');
expect(html).toContain('alt="Alt text"');
expect(html).not.toContain("<img");
expect(html).toContain("Alt text");
});
it("preserves base64 data URI images (#15437)", () => {
@@ -43,11 +42,17 @@ describe("toSanitizedMarkdownHtml", () => {
expect(html).toContain("data:image/png;base64,");
});
it("strips javascript image urls", () => {
it("flattens non-data markdown image urls", () => {
const html = toSanitizedMarkdownHtml("![X](javascript:alert(1))");
expect(html).toContain("<img");
expect(html).not.toContain("<img");
expect(html).not.toContain("javascript:");
expect(html).not.toContain("src=");
expect(html).toContain("X");
});
it("uses a plain fallback label for unlabeled markdown images", () => {
const html = toSanitizedMarkdownHtml("![](https://example.com/image.png)");
expect(html).not.toContain("<img");
expect(html).toContain("image");
});
it("renders GFM markdown tables (#20410)", () => {

View File

@@ -43,6 +43,7 @@ const MARKDOWN_CHAR_LIMIT = 140_000;
const MARKDOWN_PARSE_LIMIT = 40_000;
const MARKDOWN_CACHE_LIMIT = 200;
const MARKDOWN_CACHE_MAX_CHARS = 50_000;
const INLINE_DATA_IMAGE_RE = /^data:image\/[a-z0-9.+-]+;base64,/i;
const markdownCache = new Map<string, string>();
function getCachedMarkdown(key: string): string | null {
@@ -137,6 +138,19 @@ export function toSanitizedMarkdownHtml(markdown: string): string {
// pages) as formatted output is confusing UX (#13937).
const htmlEscapeRenderer = new marked.Renderer();
htmlEscapeRenderer.html = ({ text }: { text: string }) => escapeHtml(text);
htmlEscapeRenderer.image = (token: { href?: string | null; text?: string | null }) => {
const label = normalizeMarkdownImageLabel(token.text);
const href = token.href?.trim() ?? "";
if (!INLINE_DATA_IMAGE_RE.test(href)) {
return escapeHtml(label);
}
return `<img src="${escapeHtml(href)}" alt="${escapeHtml(label)}">`;
};
function normalizeMarkdownImageLabel(text?: string | null): string {
const trimmed = text?.trim();
return trimmed ? trimmed : "image";
}
function escapeHtml(value: string): string {
return value