diff --git a/make-pdf/src/render.ts b/make-pdf/src/render.ts index 5a4e9c4c7..8cd05918d 100644 --- a/make-pdf/src/render.ts +++ b/make-pdf/src/render.ts @@ -206,6 +206,10 @@ function decodeTypographicEntities(html: string): string { * - on* event handler attributes (onclick, ONCLICK, etc.). * - href/src with javascript: scheme. * - tags with (href ignored), so the extra pass can't break plain HTML — + // regular hyperlinks are untouched (links don't fetch at print). + const neutralizeRemoteSvgHref = (fragment: string): string => + fragment.replace( + /(\s(?:xlink:)?href\s*=\s*)("([^"]*)"|'([^']*)'|[^\s>]+)/gi, + (m, pre, val, dq, sq) => { + // Decode the value the way the HTML parser will (same single-round + // decode as style attributes above), then drop the tab/newline/CR + // characters URL parsing ignores, so https and h\nttps count. + const raw = dq ?? sq ?? String(val); + const decoded = decodeStyleAttrEntities(raw).replace(/[\t\n\r]/g, ""); + return /^\s*(?:https?:)?\/\//i.test(decoded) ? `${pre}"#"` : m; + }); + s = s.replace(//gi, neutralizeRemoteSvgHref); + s = s.replace(/<(?:image|use|feimage)\b[^>]*>/gi, neutralizeRemoteSvgHref); + // srcset with a remote candidate: Chromium prefers srcset over the inlined // src, so a remote candidate fetches at print time. Strip the attribute; // local/data: srcset values are left alone. diff --git a/make-pdf/test/render-offline-sanitize.test.ts b/make-pdf/test/render-offline-sanitize.test.ts index 1a8a7f3b3..609ac02a7 100644 --- a/make-pdf/test/render-offline-sanitize.test.ts +++ b/make-pdf/test/render-offline-sanitize.test.ts @@ -149,6 +149,81 @@ describe("sanitizeUntrustedHtml (offline fetch vectors)", () => { expect(sanitizeUntrustedHtml(input)).toContain(`srcset="a.png 1x, a@2x.png 2x"`); }); + // ── Bypass regressions: SVG remote-fetch vectors ── + // The svg handling only stripped