fix: pass ?inline=1 to file/raw so HTML preview iframe renders instead of downloading

routes.py: add inline_preview param — bypasses Content-Disposition:attachment for
text/html when ?inline=1 is set, serving the file inline for the sandboxed iframe.
workspace.js: add &inline=1 to the iframe src URL.
test: add 5 static regression tests for the inline HTML preview.
This commit is contained in:
nesquena-hermes
2026-04-25 20:15:37 +00:00
parent 2c8db0293c
commit 9af82e904f
3 changed files with 70 additions and 3 deletions
+7 -2
View File
@@ -2148,9 +2148,14 @@ def _handle_file_raw(handler, parsed):
handler.send_header("Content-Type", mime)
handler.send_header("Content-Length", str(len(raw_bytes)))
handler.send_header("Cache-Control", "no-store")
# Security: force download for dangerous MIME types to prevent XSS
# Security: force download for dangerous MIME types to prevent XSS.
# Exception: ?inline=1 permits text/html to be served inline for the
# sandboxed workspace HTML preview iframe (sandbox="allow-scripts" with no
# allow-same-origin, so the iframe cannot access parent cookies/storage).
inline_preview = qs.get("inline", [""])[0] == "1"
dangerous_types = {"text/html", "application/xhtml+xml", "image/svg+xml"}
if force_download or mime in dangerous_types:
html_inline_ok = inline_preview and mime == "text/html"
if force_download or (mime in dangerous_types and not html_inline_ok):
handler.send_header(
"Content-Disposition",
_content_disposition_value("attachment", target.name),
+1 -1
View File
@@ -234,7 +234,7 @@ async function openFile(path){
// or reading other origin data. If a stricter mode is needed, remove
// allow-scripts (or add sandbox="") to disable all JS execution.
showPreview('html');
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`;
const url=`api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&inline=1`;
const iframe=$('previewHtmlIframe');
if(iframe){
iframe.src=''; // clear first to avoid stale content
+62
View File
@@ -0,0 +1,62 @@
"""Tests for inline HTML preview in workspace panel (issue #779)."""
import pytest
def _get_routes_content():
return open("api/routes.py", encoding="utf-8").read()
def _get_workspace_js():
return open("static/workspace.js", encoding="utf-8").read()
def _get_index_html():
return open("static/index.html", encoding="utf-8").read()
def test_inline_preview_param_in_file_raw():
"""?inline=1 must bypass Content-Disposition: attachment for text/html."""
content = _get_routes_content()
assert "inline_preview" in content, (
"_handle_file_raw must read the inline query parameter"
)
assert "html_inline_ok" in content, (
"_handle_file_raw must allow HTML inline when inline_preview=True"
)
def test_iframe_uses_inline_param():
"""workspace.js must pass &inline=1 when setting the preview iframe src."""
content = _get_workspace_js()
assert "inline=1" in content, (
"workspace.js must pass ?inline=1 to api/file/raw for the HTML preview iframe"
)
def test_html_preview_iframe_exists_in_html():
"""The previewHtmlIframe element must be present in index.html."""
content = _get_index_html()
assert "previewHtmlIframe" in content, (
"index.html must contain the previewHtmlIframe element"
)
def test_html_exts_defined_in_workspace_js():
"""HTML_EXTS set must include .html and .htm."""
content = _get_workspace_js()
assert "HTML_EXTS" in content, "workspace.js must define HTML_EXTS"
assert "'.html'" in content or '".html"' in content, "HTML_EXTS must include .html"
assert "'.htm'" in content or '".htm"' in content, "HTML_EXTS must include .htm"
def test_sandbox_allows_scripts_only():
"""iframe sandbox must not include allow-same-origin (XSS risk)."""
content = _get_index_html()
# Find the sandbox attribute value
import re
sandboxes = re.findall(r'sandbox="([^"]*)"', content)
preview_sandboxes = [s for s in sandboxes if "allow" in s]
for sb in preview_sandboxes:
assert "allow-same-origin" not in sb, (
"HTML preview iframe must not have allow-same-origin (would expose parent cookies)"
)