diff --git a/make-pdf/src/render.ts b/make-pdf/src/render.ts index 514fbbc89..fe96ca3b3 100644 --- a/make-pdf/src/render.ts +++ b/make-pdf/src/render.ts @@ -247,6 +247,38 @@ export function sanitizeUntrustedHtml(html: string): string { // style="url(javascript:..)" — strip javascript: inside style attrs. s = s.replace(/url\(\s*javascript:[^)]*\)/gi, "url(#)"); + // ── Offline-posture fetch vectors (no --allow-network must mean no network + // at print time; the image inliner covers only, and must keep + // seeing remote so its blocked-remote placeholder still fires) ── + + // Remote url(...) in CSS → url(#). Scoped to `; + const out = sanitizeUntrustedHtml(input); + expect(out).not.toContain("@import"); + expect(out).not.toContain("evil.example"); + expect(out).toContain("color: red"); + }); + + test("strips string-form @import (no url())", () => { + const out = sanitizeUntrustedHtml(``); + expect(out).not.toContain("@import"); + expect(out).not.toContain("evil.example"); + }); + + test("neutralizes remote url() inside `; + const out = sanitizeUntrustedHtml(input); + expect(out).not.toContain("evil.example"); + expect(out).toContain("url(#)"); + expect(out).toContain("color: blue"); + }); + + test("neutralizes remote url() in inline style attributes", () => { + const input = `
x
`; + const out = sanitizeUntrustedHtml(input); + expect(out).not.toContain("evil.example"); + expect(out).toContain("url(#)"); + expect(out).toContain("padding:4px"); + }); + + test("neutralizes protocol-relative url(//…) in style attributes", () => { + const out = sanitizeUntrustedHtml(`
x
`); + expect(out).not.toContain("evil.example"); + }); + + test("strips srcset with a remote candidate, leaves src for the inliner", () => { + const input = ``; + const out = sanitizeUntrustedHtml(input); + expect(out).not.toContain("srcset"); + expect(out).not.toContain("evil.example"); + expect(out).toContain(`src="local.png"`); + }); + + test("strips unquoted remote srcset", () => { + const out = sanitizeUntrustedHtml(``); + expect(out).not.toContain("evil.example"); + }); + + test("keeps local-only srcset (no network vector)", () => { + const input = ``; + expect(sanitizeUntrustedHtml(input)).toContain(`srcset="a.png 1x, a@2x.png 2x"`); + }); + + test("neutralizes remote