import { describe, expect, mock, test } from "bun:test";
mock.module("cloudflare:workers", () => ({
env: { SECRETS_KEY: "test-secret", PUBLIC_URL: "https://forge.example.com" },
}));
const { renderMarkdown } = await import("./markdown");
const { verifyCamoPath } = await import("./camo-sign");
const context = { owner: "acme", repo: "app", ref: "main", basePath: "docs" };
function camoTargets(html: string): string[] {
return [...html.matchAll(/src="(\/camo\/[^"]+)"/g)].map((match) => verifyCamoPath(match[1]) ?? "INVALID");
}
describe("renderMarkdown hardening", () => {
test("external markdown images are rewritten to signed camo URLs", () => {
const html = renderMarkdown("", context);
expect(html).not.toContain("https://evil.example/pixel.gif");
expect(camoTargets(html)).toEqual(["https://evil.example/pixel.gif"]);
});
test("raw and protocol-relative sources are proxied too", () => {
const html = renderMarkdown('
![]()
', context);
expect(camoTargets(html)).toEqual(["https://tracker.example/p.png", "http://a.example/b.jpg"]);
});
test("slashless and backslash external images are proxied, not loaded directly", () => {
const html = renderMarkdown(" ", context);
expect(html).not.toMatch(/src="https?:/);
expect(camoTargets(html)).toEqual(["http://evil.example/a.gif", "https://evil.example/b.gif"]);
const bare = renderMarkdown("", null);
expect(bare).not.toContain('src="/\\evil');
});
test("entity-encoded raw image sources are decoded before the camo decision", () => {
const html = renderMarkdown(
'
',
null,
);
expect(html).not.toContain('evil.example/x.gif"');
expect(html).not.toMatch(/src="(?:https?:)?\/\/evil/);
expect(camoTargets(html)).toEqual(["https://evil.example/x.gif", "https://evil.example/y.gif"]);
});
test("absolute images on the forge itself load directly instead of disappearing", () => {
const html = renderMarkdown("", context);
expect(html).toContain('src="https://forge.example.com/acme/app/raw/main/logo.png"');
});
test("raw external anchors always get rel, even with rel= text in another attribute", () => {
const html = renderMarkdown('x', context);
expect(html).toContain('rel="nofollow noopener noreferrer"');
});
test("relative, attachment and data images stay same-origin", () => {
const html = renderMarkdown(
'  ',
context,
);
expect(html).toContain('src="/acme/app/raw/main/docs/img.png"');
expect(html).toContain('src="/attachments/abc"');
expect(html).toContain('src="data:image/png;base64,AAAA"');
expect(html).not.toContain("/camo/");
});
test("user-supplied ids and names are prefixed to prevent DOM clobbering", () => {
const html = renderMarkdown('x
@oldname");
});
});