diff --git a/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx b/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx index 6efb1c300f..1b64114575 100644 --- a/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx +++ b/studio/frontend/src/components/assistant-ui/__tests__/html-svg-renderer.test.tsx @@ -35,7 +35,7 @@ describe("HtmlSvgRenderer", () => { expect(iframe.getAttribute("srcdoc") ?? iframe.srcdoc).toContain("hello"); }); - it("renders an SVG preview and strips malicious @@ -43,15 +43,22 @@ describe("HtmlSvgRenderer", () => { render(); - const previewHost = screen.getByTestId("html-svg-renderer-svg-preview"); - // The circle survives, but every script tag and on* handler must be - // stripped by DOMPurify before the SVG is inserted into the DOM. - const circle = previewHost.querySelector("circle"); - expect(circle).not.toBeNull(); - expect(circle?.getAttribute("onclick")).toBeNull(); - expect(previewHost.querySelector("script")).toBeNull(); - expect(previewHost.innerHTML.toLowerCase()).not.toContain("onclick"); - expect(previewHost.innerHTML.toLowerCase()).not.toContain("alert"); + const iframe = screen.getByTestId( + "html-svg-renderer-svg-preview", + ) as HTMLIFrameElement; + expect(iframe.tagName).toBe("IFRAME"); + // SECURITY: SVG iframe must NEVER allow scripts or same-origin -- those + // would re-introduce the host-page-leak / XSS regressions the iframe + // boundary is here to prevent. + expect(iframe.getAttribute("sandbox")).toBe(""); + const srcdoc = (iframe.getAttribute("srcdoc") ?? iframe.srcdoc).toLowerCase(); + expect(srcdoc).toContain(" { @@ -164,4 +171,28 @@ describe("sanitizeSvgSource", () => { expect(clean.startsWith(" blocks so SVG CSS cannot retarget host selectors", () => { + const svg = ``; + const clean = sanitizeSvgSource(svg).toLowerCase(); + expect(clean).not.toContain(" { + const svg = ``; + const clean = sanitizeSvgSource(svg).toLowerCase(); + expect(clean).toContain("/ tags so SVG cannot beacon to external URLs", () => { + const svg = ``; + const clean = sanitizeSvgSource(svg).toLowerCase(); + expect(clean).not.toContain("]|]|]|]|]/i; -// SVGs may legitimately reference fonts/images via href, so we keep those. -// We strip every event handler attribute (on*) and any tag DOMPurify would -// otherwise allow that could escape the SVG sandbox. +// SVG previews used to live inside a `` tag +// where the browser treats the SVG as an image and disables scripts and +// external resource loads. Mounting sanitized SVG directly into the host +// Studio document loses those guarantees, so we now (a) strip every node that +// can leak into the host page (`", + safeSvg, + ].join(""); +} + function SvgPreview({ source }: { source: string }) { - const safe = useMemo(() => sanitizeSvgSource(source), [source]); + const srcDoc = useMemo(() => buildSvgSrcDoc(sanitizeSvgSource(source)), [ + source, + ]); return ( -
); }