From 131ef143304c73067a81b94ef50593c30dd53b0e Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Wed, 3 Jun 2026 16:35:18 +0800 Subject: [PATCH 1/3] fix(dashboard): relax frame security headers when running under launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When AstrBot is launched by the AstrBot Launcher, the dashboard is embedded in a cross-origin iframe (the Tauri webview). The plugin page responses set X-Frame-Options: SAMEORIGIN and CSP frame-ancestors 'self', both of which inspect the *entire* ancestor chain — not just the immediate parent. Because the top-level Tauri webview has a different origin, these headers block the plugin page from loading inside the nested iframe, resulting in 'localhost refused to connect'. Fix: skip the restrictive frame headers when ASTRBOT_LAUNCHER=1 is set, which the launcher already injects as an environment variable. Other security measures (iframe sandbox, JWT asset_token, postMessage bridge) remain in place. --- astrbot/dashboard/routes/plugin.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 034e742848..cf11a0d1b1 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -789,14 +789,21 @@ def _apply_plugin_page_security_headers(response: QuartResponse) -> QuartRespons response.headers["Cache-Control"] = "no-store" response.headers["Referrer-Policy"] = "no-referrer" response.headers["X-Content-Type-Options"] = "nosniff" - response.headers["X-Frame-Options"] = "SAMEORIGIN" response.headers["Cross-Origin-Resource-Policy"] = "cross-origin" # Sandboxed iframes without allow-same-origin load ES modules with Origin: null. # CORS read access is allowed here; JWT/asset_token still protects the assets. response.headers["Access-Control-Allow-Origin"] = "*" - response.headers["Content-Security-Policy"] = ( - "frame-ancestors 'self'; object-src 'none'; base-uri 'self'" - ) + + # When running under the AstrBot Launcher the dashboard is embedded in a + # cross-origin iframe (the Tauri webview). Since frame-ancestors and + # X-Frame-Options inspect the *entire* ancestor chain, enforcing them here + # would block plugin pages from loading inside the nested iframe. + if not os.environ.get("ASTRBOT_LAUNCHER"): + response.headers["X-Frame-Options"] = "SAMEORIGIN" + response.headers["Content-Security-Policy"] = ( + "frame-ancestors 'self'; object-src 'none'; base-uri 'self'" + ) + return response async def _serve_plugin_page_html_asset( From c3b0835ab4355d4f71cb6e1050ccea388dfa37bd Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Wed, 3 Jun 2026 16:54:37 +0800 Subject: [PATCH 2/3] fix(dashboard): preserve object-src and base-uri CSP directives under launcher Keep object-src 'none' and base-uri 'self' in the CSP header even when ASTRBOT_LAUNCHER is set. Only frame-ancestors and X-Frame-Options need to be relaxed because the Tauri webview is a cross-origin ancestor. --- astrbot/dashboard/routes/plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index cf11a0d1b1..282ff6e6e4 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -803,6 +803,10 @@ def _apply_plugin_page_security_headers(response: QuartResponse) -> QuartRespons response.headers["Content-Security-Policy"] = ( "frame-ancestors 'self'; object-src 'none'; base-uri 'self'" ) + else: + response.headers["Content-Security-Policy"] = ( + "object-src 'none'; base-uri 'self'" + ) return response From 0ff555c19ab44931867adcc513e7574b7c2253d4 Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Wed, 3 Jun 2026 17:02:13 +0800 Subject: [PATCH 3/3] fix(dashboard): tighten ASTRBOT_LAUNCHER check and always emit CSP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use explicit value check ('1' / 'true') instead of truthiness for the ASTRBOT_LAUNCHER env var. Always emit a Content-Security-Policy header and only conditionally prepend frame-ancestors 'self' — this keeps object-src 'none' and base-uri 'self' active under the launcher. --- astrbot/dashboard/routes/plugin.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 282ff6e6e4..b857f7832f 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -798,15 +798,11 @@ def _apply_plugin_page_security_headers(response: QuartResponse) -> QuartRespons # cross-origin iframe (the Tauri webview). Since frame-ancestors and # X-Frame-Options inspect the *entire* ancestor chain, enforcing them here # would block plugin pages from loading inside the nested iframe. - if not os.environ.get("ASTRBOT_LAUNCHER"): + csp = "object-src 'none'; base-uri 'self'" + if os.environ.get("ASTRBOT_LAUNCHER") not in ("1", "true"): response.headers["X-Frame-Options"] = "SAMEORIGIN" - response.headers["Content-Security-Policy"] = ( - "frame-ancestors 'self'; object-src 'none'; base-uri 'self'" - ) - else: - response.headers["Content-Security-Policy"] = ( - "object-src 'none'; base-uri 'self'" - ) + csp = f"frame-ancestors 'self'; {csp}" + response.headers["Content-Security-Policy"] = csp return response