From 8bc2677691092014180ec459186ddd984cd06c01 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Thu, 7 May 2026 17:07:38 +0800 Subject: [PATCH] fix: repair file picker and html preview interactions --- api/routes.py | 21 ++++- static/boot.js | 2 +- static/index.html | 4 +- static/style.css | 1 + static/ui.js | 12 ++- .../test_issue1800_file_html_interactions.py | 83 +++++++++++++++++++ tests/test_media_inline.py | 33 ++++++++ 7 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 tests/test_issue1800_file_html_interactions.py diff --git a/api/routes.py b/api/routes.py index e644b193..e086587a 100644 --- a/api/routes.py +++ b/api/routes.py @@ -5070,8 +5070,17 @@ def _serve_file_bytes(handler, target: Path, mime: str, disposition: str, cache_ handler.send_header("Cache-Control", cache_control) handler.send_header("Content-Disposition", _content_disposition_value(disposition, target.name)) if csp: + # Sandboxed inline HTML must remain frameable for workspace previews; + # X-Frame-Options: DENY would block the iframe before CSP sandbox applies. handler.send_header("Content-Security-Policy", csp) - _security_headers(handler) + handler.send_header("X-Content-Type-Options", "nosniff") + handler.send_header("Referrer-Policy", "same-origin") + handler.send_header( + "Permissions-Policy", + "camera=(), microphone=(self), geolocation=(), clipboard-write=(self)", + ) + else: + _security_headers(handler) handler.end_headers() if content_length: @@ -5157,8 +5166,9 @@ def _handle_media(handler, parsed): ext = target.suffix.lower() mime = MIME_MAP.get(ext, "application/octet-stream") - # Only serve safe media/PDF types inline when explicitly requested. Everything - # else remains a download. SVG is always a download (XSS risk). + # Only serve safe media/PDF types inline when explicitly requested. HTML is + # allowed inline only with a CSP sandbox so "open full page" can work without + # granting same-origin access to the WebUI. SVG is always a download (XSS risk). _INLINE_IMAGE_TYPES = { "image/png", "image/jpeg", "image/gif", "image/webp", "image/x-icon", "image/bmp", @@ -5171,12 +5181,15 @@ def _handle_media(handler, parsed): } _DOWNLOAD_TYPES = {"image/svg+xml"} # SVG: XSS risk, force download inline_preview = qs.get("inline", [""])[0] == "1" + html_inline_ok = inline_preview and mime == "text/html" disposition = "inline" if ( mime not in _DOWNLOAD_TYPES and ( mime in _INLINE_IMAGE_TYPES or (inline_preview and mime in _INLINE_PREVIEW_TYPES) + or html_inline_ok ) ) else "attachment" - return _serve_file_bytes(handler, target, mime, disposition, "private, max-age=3600") + csp = "sandbox allow-scripts" if html_inline_ok else None + return _serve_file_bytes(handler, target, mime, disposition, "private, max-age=3600", csp=csp) def _handle_file_raw(handler, parsed): diff --git a/static/boot.js b/static/boot.js index 3fa54c94..e6f1b7e7 100644 --- a/static/boot.js +++ b/static/boot.js @@ -267,7 +267,7 @@ $('btnSend').onclick=()=>{ } send(); }; -$('btnAttach').onclick=()=>$('fileInput').click(); +$('btnAttach').onclick=e=>{if(e&&e.preventDefault)e.preventDefault();$('fileInput').value='';$('fileInput').click();}; // ── Voice input (Web Speech API + MediaRecorder fallback) ─────────────────── (function(){ diff --git a/static/index.html b/static/index.html index 2eb061ad..ac90bd8b 100644 --- a/static/index.html +++ b/static/index.html @@ -465,8 +465,8 @@