/**
* 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