diff --git a/api/routes.py b/api/routes.py index 23994f04..9a604ddc 100644 --- a/api/routes.py +++ b/api/routes.py @@ -8677,10 +8677,11 @@ def _handle_file_raw(handler, parsed): # CSP sandbox directive applies the same isolation server-side: without # allow-same-origin, the document is treated as a unique opaque origin and # cannot read WebUI cookies, localStorage, or postMessage to the parent. - csp = "sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox" if html_inline_ok else None + sandbox_csp = "sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox" + csp = sandbox_csp if (inline_preview and not force_download and disposition == "inline") else None # _serve_file_bytes sends Content-Security-Policy when csp is set. if html_inline_ok: - return _serve_inline_html_preview(handler, target, "no-store", csp=csp) + return _serve_inline_html_preview(handler, target, "no-store", csp=sandbox_csp) return _serve_file_bytes(handler, target, mime, disposition, "no-store", csp=csp) diff --git a/static/workspace.js b/static/workspace.js index 4a2eefb4..316595bb 100644 --- a/static/workspace.js +++ b/static/workspace.js @@ -675,6 +675,6 @@ function renderFileBreadcrumb(filePath) { function openInBrowser(){ if(!_previewCurrentPath||!S.session) return; - const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(_previewCurrentPath)}`; - window.open(url,'_blank'); + const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(_previewCurrentPath)}&inline=1`; + window.open(url,'_blank','noopener'); } diff --git a/tests/test_779_html_preview.py b/tests/test_779_html_preview.py index f3dcd876..e8feea22 100644 --- a/tests/test_779_html_preview.py +++ b/tests/test_779_html_preview.py @@ -33,6 +33,23 @@ def test_iframe_uses_inline_param(): ) +def test_open_in_browser_uses_unconditional_inline_param(): + """Workspace Open in browser must use the explicit inline/sandbox open path.""" + content = _get_workspace_js() + idx = content.find("function openInBrowser()") + assert idx != -1, "openInBrowser() must exist" + block = content[idx:idx + 700] + assert "&inline=1" in block, ( + "openInBrowser() must include inline=1 so HTML opens instead of downloading" + ) + assert "download=1" not in block, ( + "openInBrowser() must not reuse the Download button path" + ) + assert "window.open(url,'_blank','noopener')" in block or 'window.open(url,"_blank","noopener")' in block, ( + "openInBrowser() should use noopener for the new tab" + ) + + def test_html_preview_iframe_exists_in_html(): """The previewHtmlIframe element must be present in index.html.""" content = _get_index_html() @@ -105,3 +122,15 @@ def test_inline_html_response_sets_csp_sandbox(): assert "allow-same-origin" not in line, ( "CSP sandbox must NOT include allow-same-origin — that would defeat the isolation" ) + + +def test_file_raw_inline_responses_use_sandbox_csp(): + """The explicit inline-open contract should sandbox non-download raw files.""" + content = _get_routes_content() + idx = content.find("def _handle_file_raw") + assert idx != -1, "_handle_file_raw not found" + block = content[idx:idx + 2200] + assert "sandbox_csp" in block + assert "inline_preview" in block + assert "disposition == \"inline\"" in block + assert "csp=sandbox_csp" in block or "csp=csp" in block