/** * Offline-posture sanitizer tests — raw-HTML fetch vectors beyond * (which the image inliner owns). No Playwright, no PDF generation. * * Regression for: `; 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"); }); // ── Bypass regressions: unquoted style attributes ── // HTML spec: an unquoted attribute value runs until whitespace or `>`, so //
is live markup Chromium honors. // The original neutralizer only rewrote quoted values. test("neutralizes remote url() in UNQUOTED style attributes", () => { const out = sanitizeUntrustedHtml(`
x
`); expect(out).not.toContain("evil.example"); expect(out).toContain("url(#)"); }); test("keeps local url() in unquoted style attributes functional", () => { const out = sanitizeUntrustedHtml(`
x
`); expect(out).toContain("url(local.png)"); }); // ── Bypass regressions: CSS-escape obfuscation ── // Chromium decodes CSS ident/string escapes before fetching, so \69 → i and // \68 → h defeat literal-pattern matching. Untrusted styling has no // legitimate need for escaped url schemes or at-rule names — fail closed. test("drops CSS-escaped @import (@\\69mport url(...)) in `); expect(out).not.toContain("evil.example"); expect(out).not.toMatch(/@\\/); // no escaped at-rule survives for Chromium to decode }); test("drops CSS-escaped string-form @import (@\\69mport \"https://…\")", () => { const out = sanitizeUntrustedHtml(``); expect(out).not.toContain("evil.example"); expect(out).not.toMatch(/@\\/); }); test("neutralizes CSS-escaped scheme inside url() (\\68ttps://…)", () => { const out = sanitizeUntrustedHtml(``); expect(out).not.toContain("evil.example"); }); test("neutralizes CSS-escaped function names (u\\72l(https://…))", () => { const out = sanitizeUntrustedHtml(``); expect(out).not.toContain("evil.example"); }); test("neutralizes HTML-entity-encoded backslash escapes in style attributes", () => { // Attribute values are entity-decoded by the HTML parser before the CSS // parser runs, so \68ttps reaches Chromium as \68ttps → https. const out = sanitizeUntrustedHtml(`
x
`); expect(out).not.toContain("evil.example"); }); // ── Bypass regressions: non-backslash entity obfuscation in style attrs ── // The same attribute entity layer can hide ANY character of a fetch vector, // not just backslashes: h → h, / → /. `; const out = sanitizeUntrustedHtml(input); expect(out).not.toContain("evil.example"); expect(out).toContain("url(#)"); expect(out).toContain("color: blue"); }); test("neutralizes remote image-set(...) in style attributes", () => { const out = sanitizeUntrustedHtml(`
x
`); expect(out).not.toContain("evil.example"); }); test("neutralizes -webkit-image-set with a remote string argument", () => { const out = sanitizeUntrustedHtml(``); expect(out).not.toContain("evil.example"); }); test("keeps local image-set(...) functional", () => { const input = ``; expect(sanitizeUntrustedHtml(input)).toContain(`image-set("local.png" 1x, "local@2x.png" 2x)`); }); test("neutralizes remote