拒绝桌面壳默认下载落盘

桌面壳 WebView 下载事件默认拒绝网页触发的落盘动作

桌面壳配置检查守卫下载拒绝策略

宿主壳方案和共享决策记录桌面保存边界
This commit is contained in:
2026-06-18 10:49:40 +08:00
parent db4df0ab9a
commit 16196464b9
4 changed files with 38 additions and 1 deletions
@@ -529,6 +529,10 @@ const requiredMainSnippets = [
'open_desktop_external_navigation',
'.on_navigation(move |url|',
'.on_new_window(move |url, _features|',
'DownloadEvent',
'should_allow_desktop_webview_download',
'DownloadEvent::Requested { .. } => false',
'.on_download(|_webview, event| should_allow_desktop_webview_download(&event))',
'NewWindowResponse::Deny',
];
+31 -1
View File
@@ -8,7 +8,7 @@ use std::sync::Mutex;
use std::time::Duration;
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::webview::NewWindowResponse;
use tauri::webview::{DownloadEvent, NewWindowResponse};
use tauri::DragDropEvent;
use tauri::Manager;
use tauri::Theme;
@@ -326,6 +326,14 @@ fn open_desktop_external_navigation(app: &tauri::AppHandle, url: &Url) {
let _ = app.opener().open_url(external_url, None::<&str>);
}
fn should_allow_desktop_webview_download(event: &DownloadEvent<'_>) -> bool {
match event {
DownloadEvent::Requested { .. } => false,
DownloadEvent::Finished { .. } => true,
_ => false,
}
}
fn normalize_native_page_url(raw_url: &str) -> Option<Url> {
let url = raw_url.trim();
if url.is_empty() || url.chars().any(char::is_control) {
@@ -1636,6 +1644,7 @@ fn main() {
open_desktop_external_navigation(&new_window_app_handle, &url);
NewWindowResponse::Deny
})
.on_download(|_webview, event| should_allow_desktop_webview_download(&event))
.build()?;
register_desktop_window_close_events(&window, tray_registered);
register_desktop_lifecycle_events(&window);
@@ -2025,6 +2034,27 @@ mod tests {
assert_eq!(desktop_external_navigation_url(&file_url), None);
}
#[test]
fn desktop_webview_downloads_are_blocked_by_default() {
let mut destination = std::env::temp_dir().join("genarrative-webview-download.txt");
let url =
Url::parse("https://app.genarrative.world/download.txt").expect("download url");
let requested = DownloadEvent::Requested {
url: url.clone(),
destination: &mut destination,
};
assert!(!should_allow_desktop_webview_download(&requested));
let finished = DownloadEvent::Finished {
url,
path: None,
success: false,
};
assert!(should_allow_desktop_webview_download(&finished));
}
#[test]
fn native_page_url_normalization_allows_same_origin_routes() {
assert_eq!(
@@ -58,6 +58,7 @@
- 2026-06-18 桌面壳安装包身份:Tauri 桌面壳的产品名固定为 `Genarrative`,应用 identifier 固定为 `world.genarrative.desktop`Tauri 配置、`apps/desktop-shell/package.json` 与 Cargo package 版本统一为 `0.1.0`Release 主窗口只加载打包的 `index.html` 和根 `dist` H5 资产,dev URL 只指向本机 Vite 调试入口。桌面壳 CSP 保持 `script-src 'self'`,不得加入 `unsafe-eval``tauri:``file:`,也不得在没有真实端点、签名密钥和发布流程前配置 updater;检查脚本会拒绝包身份、版本、CSP 或 updater 约束漂移。
- 2026-06-18 桌面壳运行时平台 query:Tauri 静态配置中的 `hostPlatform=unknown` 只作为跨平台构建模板值;Rust `setup` 手动创建主窗口前必须把入口 URL 改写为当前 `macos` / `windows` / `linux`,保证 H5 首屏 query 与 `host.getRuntime` 回读的平台一致。第二实例参数、外部 deep link 或 H5 自报值不得覆盖该字段;桌面壳测试和配置检查会拒绝绕过该归一流程。
- 2026-06-18 桌面壳顶层导航边界:Tauri 主 WebView 只允许打包资产 URL 和 `https://app.genarrative.world` 同源 H5 route 留在主窗口;外域 `http:` / `https:``mailto:``tel:` 导航与 `window.open` 请求交给系统 opener 后拒绝 WebView 留壳;`javascript:``file:` 等危险协议直接拒绝。该规则不进入 HostBridge capability,不开放 opener JS guest API,配置检查和 cargo test 覆盖导航策略。
- 2026-06-18 桌面壳默认下载边界:Tauri 主 WebView 的下载事件默认拒绝网页自动下载和 `<a download>` 落盘,桌面文件保存只能通过 `file.exportText``file.exportImage``file.exportAudio` 等已声明 HostBridge method 进入 Rust 侧系统保存对话框,并继续执行 MIME、大小、文件名清洗和用户确认。该规则不进入 HostBridge capability,配置检查和 cargo test 覆盖下载拒绝策略。
- 2026-06-18 桌面壳 Tauri 命令白名单:桌面壳源码、Tauri build manifest、主窗口 capability 和本地自动生成权限目录都只能暴露 `host_bridge_request` 一个受控 command;所有桌面能力继续在 Rust 内部按 HostBridge method 白名单分发,不新增可被 H5 直接 `invoke` 的 Tauri command,也不授予插件 JS guest API。检查脚本会拒绝多余 command、权限列表顺序漂移和残留的自动生成权限文件。
- 2026-06-18 桌面壳 CSP 分层:Tauri release `csp` 不得包含 `http://127.0.0.1:*``ws://127.0.0.1:*` 或其它本机调试源,本机 Vite、HMR WebSocket 和开发 frame 只允许出现在 `devCsp`。桌面壳配置检查会同时拒绝 release CSP 混入本机调试源、dev CSP 缺失本机开发源,以及 release / dev CSP 加入 `unsafe-eval``tauri:``file:`
- 2026-06-18 壳生产代码禁用临时替身:Expo 与 Tauri 壳的生产源码和配置不得出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 mock。两端壳配置检查会扫描生产入口、配置和壳实现,防止把临时替身、占位文案或伪实现带进可分发壳。
@@ -179,6 +179,7 @@ Tauri 壳同样只负责桌面宿主能力,不承接玩法业务。
- Tauri capabilities 只授予主窗口所需命令;默认不开放文件系统、shell、全局剪贴板或任意插件能力。
- 桌面支付首期走现有 H5 / 二维码 / 外部浏览器路径,不在 Rust 侧保存支付凭据。
- 文件导出、作品卡保存、图片拖拽导入、系统托盘、自动更新等桌面能力按后续需求逐项开放;其中系统托盘属于桌面壳自有能力,不作为 HostBridge method 暴露给 H5。
- 主 WebView 默认拒绝网页自动下载或 `<a download>` 触发的落盘动作;用户保存文本、图片、音频等内容必须走已声明的 `file.exportText``file.exportImage``file.exportAudio` HostBridge method,由 Rust 侧执行 MIME、大小、文件名清洗和系统保存对话框确认。
桌面 release 和 dev 模式:
@@ -233,6 +234,7 @@ GameBridge 禁止:
- 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin``payment.request` 的伪声明。
- 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。
- Tauri 禁止把 shell / fs 等高危插件作为默认能力暴露给主 WebView。
- Tauri 主 WebView 禁止默认下载落盘;桌面文件保存只能通过受控 HostBridge 导出能力进入系统保存对话框。
- RN WebView 禁止打开任意 URL 后仍保留完整 HostBridge;跳外链只允许 `http:``https:``mailto:``tel:`,并使用系统浏览器或降级能力,危险协议直接阻断。
- AI sandbox iframe 必须使用独立 CSP、`sandbox` 属性和单独 GameBridge allowlist。