From 0930e08496c1d94977bae3acab73c58bbd0aa3c9 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Wed, 3 Jun 2026 12:01:54 +0400 Subject: [PATCH] RAG preview: serve preview files frame-safe for the native PDF viewer --- studio/backend/main.py | 14 +++++++++++++- studio/backend/routes/rag.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/studio/backend/main.py b/studio/backend/main.py index 67cc7539a3..2f12557f82 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -440,6 +440,18 @@ _CSP_SCRIPT_NONCE_HEADER = "x-internal-script-nonce" _ARTIFACT_PREVIEW_FRAME_PATH = "/api/inference/artifact-preview-frame" +def _is_frameable_path(path: str) -> bool: + """Paths allowed to be embedded in a same-origin / Tauri iframe. These + responses set their own CSP frame-ancestors; X-Frame-Options: DENY would + override that, so it is skipped for them.""" + if path == _ARTIFACT_PREVIEW_FRAME_PATH: + return True + # RAG PDF preview is shown in the browser's native viewer inside an iframe. + return path.startswith("/api/rag/documents/") and path.endswith( + ("/file", "/file-signed") + ) + + # /content is Colab's working directory — more reliable than env vars which # aren't always set depending on Colab runtime version. import importlib.util as _importlib_util @@ -510,7 +522,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): response.headers.setdefault("Content-Security-Policy", _build_csp(nonce)) # Omit X-Frame-Options in Colab — CSP frame-ancestors handles it, and # DENY would block serve_kernel_port_as_iframe regardless of CSP. - if not _IS_COLAB and request.url.path != _ARTIFACT_PREVIEW_FRAME_PATH: + if not _IS_COLAB and not _is_frameable_path(request.url.path): response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("Referrer-Policy", "no-referrer") diff --git a/studio/backend/routes/rag.py b/studio/backend/routes/rag.py index ebbc327346..96b95efe94 100644 --- a/studio/backend/routes/rag.py +++ b/studio/backend/routes/rag.py @@ -1181,6 +1181,12 @@ def _iter_file_range(path: Path, start: int, end: int): yield chunk +# Origins allowed to embed the preview file in an iframe: the web app (same +# origin) and the desktop Tauri webview. Mirrors routes/inference.py's +# artifact-preview frame ancestors. +_PREVIEW_FRAME_ANCESTORS = "'self' tauri://localhost http://tauri.localhost" + + def _serve_document_file_row( doc_row: Any, document_id: str, @@ -1194,6 +1200,11 @@ def _serve_document_file_row( "X-Content-Type-Options": "nosniff", "Cache-Control": "private, max-age=0, must-revalidate", "Accept-Ranges": "bytes", + # The PDF preview embeds this file in a same-origin / Tauri iframe shown + # in the browser's native viewer. The global SecurityHeadersMiddleware + # sets frame-ancestors 'none'; override it here (and this path is exempt + # from X-Frame-Options: DENY in main.py) so the iframe can load. + "Content-Security-Policy": f"frame-ancestors {_PREVIEW_FRAME_ANCESTORS}", } size = resolved.stat().st_size