mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-29 13:10:17 +00:00
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:
+7
-2
@@ -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
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
)
|
||||
Reference in New Issue
Block a user