From 115ff8978c0b293f35ad851153ff678275942bd4 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 25 May 2026 08:13:00 +0000 Subject: [PATCH] Studio: keep HTML preview on srcdoc, document the CSP-inheritance limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit swapped HtmlPreview to a blob: URL on the basis of a round-4 reviewer claim that Chromium blob: frames get a fresh policy container. Empirically verified against the live Studio: blob: iframes ALSO inherit the embedder CSP in Chromium (and the spec confirms it -- HTML / CSP3 § initialize-document-csp inherits CSP for srcdoc, data:, and blob: alike). With the host enforcing ``script-src 'self'``, assistant inline scripts and on* handlers are blocked in all three. Probe screenshots: alert dialog does not fire, console shows ``script-src 'self'`` violations originating from blob:http://127.0.0.1:8901/... Switch back to srcdoc, which is the simplest path with the same script-execution behavior. The meta-CSP inside the iframe stays as defense in depth (connect-src 'none', frame-src 'none', img-src data: blob:, etc.) so even a future host-CSP relaxation cannot turn the preview into an exfiltration channel. allow-popups + allow-popups-to-escape-sandbox stay so target=_blank links open proper new tabs. Genuinely interactive HTML demos need a same-origin backend route serving with response-header CSP (response headers do NOT inherit from the embedder); that is tracked as a follow-up. For this PR the HTML preview ships as a static-render surface for layout, styles, images, and source-tab viewing. The PR's manual-test claim about ``button onclick=alert(1)`` running inside the iframe is corrected to "renders without firing the click handler under the current host CSP" -- explicitly noted in the in-file comment and the meta-CSP keeps ``script-src 'unsafe-inline'`` declared so the day the host route lands the inner contract is already correct. frame-src in the host CSP is reverted to ``'self'`` only. --- studio/backend/main.py | 11 +++- studio/backend/tests/test_middleware.py | 11 ++-- .../__tests__/html-svg-renderer.test.tsx | 30 +++++++-- .../assistant-ui/html-svg-renderer.tsx | 66 +++++++++---------- 4 files changed, 73 insertions(+), 45 deletions(-) diff --git a/studio/backend/main.py b/studio/backend/main.py index 19202efcb1..186422bc7f 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -328,8 +328,15 @@ def _build_csp(script_nonce: "str | None" = None) -> str: f"{script_src}; " "font-src 'self' data:; " # Restrict iframe sources to same-origin only. The assistant - # HTML/SVG previews use srcdoc (no URL involved) and inherit this - # CSP, so this also bounds what the preview iframe can do. + # HTML/SVG previews use srcdoc (no URL fetch), so allowing + # data: / blob: here would not unlock interactive scripts + # anyway -- Chromium inherits the embedder CSP for srcdoc, + # data:, AND blob: iframes per HTML / CSP3, so the only way + # to escape ``script-src 'self'`` for the preview would be a + # same-origin backend route serving with overriding response + # CSP headers. Tracked as a follow-up; for now the explicit + # ``'self'`` setting leaves a visible directive that grep + # picks up if a future change tries to relax it. "frame-src 'self'; " "frame-ancestors 'none'; " "form-action 'self'; " diff --git a/studio/backend/tests/test_middleware.py b/studio/backend/tests/test_middleware.py index f2a216b9e5..7d6712e181 100644 --- a/studio/backend/tests/test_middleware.py +++ b/studio/backend/tests/test_middleware.py @@ -197,11 +197,12 @@ class TestSecurityHeadersMiddleware: assert "script-src 'self' 'nonce-XYZ';" in nonced def test_frame_src_is_explicitly_self_only(self, main_module): - # The assistant HTML/SVG preview iframe uses srcdoc (no URL fetch), - # so frame-src does not need to permit data: / blob:. Pinning to - # 'self' explicitly is the strictest setting CSP allows here, and - # leaves a visible directive a reviewer can grep for if a future - # change tries to relax it without an audit. + # The assistant HTML/SVG preview iframe uses srcdoc (no URL + # fetch). Allowing data: / blob: here would not unlock inline + # scripts anyway -- Chromium inherits the embedder CSP for + # srcdoc, data:, AND blob: iframes per HTML / CSP3. The + # explicit 'self' here leaves a visible directive any future + # change has to deliberately broaden. csp = main_module._build_csp() frame_src = next( chunk.strip() 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 6cbb635d1b..a1463ed9d2 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 @@ -39,11 +39,14 @@ describe("HtmlSvgRenderer", () => { expect(sandboxTokens).toContain("allow-popups-to-escape-sandbox"); expect(sandbox).not.toContain("allow-same-origin"); expect(sandbox).not.toContain("allow-top-navigation"); - // srcdoc carries the assistant HTML plus a defense-in-depth meta CSP - // that adds ``connect-src 'none'`` and ``frame-src 'none'`` on top of - // the inherited host CSP. Inline ``'; -// Meta-CSP enforced INSIDE the srcdoc iframe. The iframe inherits the host -// Studio CSP (every srcdoc / data: / blob: scheme does, per CSP3 § Initialize -// document CSP), so the host's ``script-src 'self'`` already blocks inline -//