From ff438e88970cdf463820cc4c5214ef9b2294567c Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 12:00:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E9=97=AD=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E7=BD=91=E9=A1=B5=E4=B8=8B=E8=BD=BD=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳 WebView 注入下载阻断脚本并丢弃 iOS 文件下载事件。 移动壳 Android 包配置阻断外部存储和请求安装包权限。 移动壳配置检查和 Expo public config smoke 锁定下载边界。 新增移动壳 WebView 下载阻断策略测试。 更新宿主壳方案和共享决策日志记录移动端下载只能走 HostBridge 导出。 --- apps/mobile-shell/App.tsx | 3 +++ apps/mobile-shell/app.json | 6 +++++- apps/mobile-shell/scripts/check-config.mjs | 7 +++++++ .../scripts/check-expo-config.mjs | 20 +++++++++++++++++++ .../src/mobileShellWebViewPolicy.test.ts | 16 +++++++++++++++ .../src/mobileShellWebViewPolicy.ts | 13 ++++++++++++ .../shared-memory/decision-log.md | 1 + ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 3 +++ 8 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 apps/mobile-shell/src/mobileShellWebViewPolicy.test.ts create mode 100644 apps/mobile-shell/src/mobileShellWebViewPolicy.ts diff --git a/apps/mobile-shell/App.tsx b/apps/mobile-shell/App.tsx index 30cd3c811..e6418ef61 100644 --- a/apps/mobile-shell/App.tsx +++ b/apps/mobile-shell/App.tsx @@ -31,6 +31,7 @@ import { buildMobileShellUrl, resolveMobileShellBaseWebUrl, } from './src/mobileShellUrl'; +import { BLOCK_WEBVIEW_DOWNLOAD_SCRIPT } from './src/mobileShellWebViewPolicy'; function buildHostBridgeMessageScript(message: unknown) { return `window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify( @@ -199,6 +200,8 @@ export default function App() { thirdPartyCookiesEnabled={false} sharedCookiesEnabled={false} webviewDebuggingEnabled={false} + injectedJavaScriptBeforeContentLoaded={BLOCK_WEBVIEW_DOWNLOAD_SCRIPT} + onFileDownload={() => undefined} onMessage={handleMessage} onShouldStartLoadWithRequest={handleShouldStartLoad} onNavigationStateChange={(event) => { diff --git a/apps/mobile-shell/app.json b/apps/mobile-shell/app.json index 6a96b0c94..005394c7d 100644 --- a/apps/mobile-shell/app.json +++ b/apps/mobile-shell/app.json @@ -55,10 +55,14 @@ "allowBackup": false, "softwareKeyboardLayoutMode": "resize", "blockedPermissions": [ + "android.permission.MANAGE_EXTERNAL_STORAGE", + "android.permission.READ_EXTERNAL_STORAGE", "android.permission.RECORD_AUDIO", "android.permission.RECEIVE_BOOT_COMPLETED", + "android.permission.REQUEST_INSTALL_PACKAGES", "android.permission.SCHEDULE_EXACT_ALARM", - "android.permission.USE_EXACT_ALARM" + "android.permission.USE_EXACT_ALARM", + "android.permission.WRITE_EXTERNAL_STORAGE" ], "adaptiveIcon": { "foregroundImage": "./assets/icon.png", diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 8f585ad9b..f99f22922 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -72,10 +72,14 @@ const blockedMobileChannelSnippets = [ 'posthog.init', ]; const blockedAndroidPermissions = [ + 'android.permission.MANAGE_EXTERNAL_STORAGE', + 'android.permission.READ_EXTERNAL_STORAGE', 'android.permission.RECORD_AUDIO', 'android.permission.RECEIVE_BOOT_COMPLETED', + 'android.permission.REQUEST_INSTALL_PACKAGES', 'android.permission.SCHEDULE_EXACT_ALARM', 'android.permission.USE_EXACT_ALARM', + 'android.permission.WRITE_EXTERNAL_STORAGE', ]; function extractStringArrayExport(source, exportName) { @@ -425,6 +429,7 @@ for (const snippet of [ 'subscribeMobileNetworkStatus', 'navigation.canGoBack', 'buildHostBridgeMessageScript', + 'BLOCK_WEBVIEW_DOWNLOAD_SCRIPT', 'SafeAreaProvider', 'SafeAreaView', 'MOBILE_SHELL_SAFE_AREA_EDGES', @@ -440,6 +445,8 @@ for (const snippet of [ 'thirdPartyCookiesEnabled={false}', 'sharedCookiesEnabled={false}', 'webviewDebuggingEnabled={false}', + 'injectedJavaScriptBeforeContentLoaded={BLOCK_WEBVIEW_DOWNLOAD_SCRIPT}', + 'onFileDownload={() => undefined}', 'setSupportMultipleWindows={false}', ]) { if (!appSource.includes(snippet)) { diff --git a/apps/mobile-shell/scripts/check-expo-config.mjs b/apps/mobile-shell/scripts/check-expo-config.mjs index 692f26335..22ae2a2f0 100644 --- a/apps/mobile-shell/scripts/check-expo-config.mjs +++ b/apps/mobile-shell/scripts/check-expo-config.mjs @@ -135,11 +135,31 @@ assertEqual( 'resize', 'Android software keyboard layout mode', ); +assertIncludes( + expoConfig.android?.blockedPermissions, + 'android.permission.MANAGE_EXTERNAL_STORAGE', + 'Android external download blocked permissions', +); +assertIncludes( + expoConfig.android?.blockedPermissions, + 'android.permission.READ_EXTERNAL_STORAGE', + 'Android external download blocked permissions', +); assertIncludes( expoConfig.android?.blockedPermissions, 'android.permission.RECORD_AUDIO', 'Android blocked permissions', ); +assertIncludes( + expoConfig.android?.blockedPermissions, + 'android.permission.REQUEST_INSTALL_PACKAGES', + 'Android external install blocked permissions', +); +assertIncludes( + expoConfig.android?.blockedPermissions, + 'android.permission.WRITE_EXTERNAL_STORAGE', + 'Android external download blocked permissions', +); for (const permission of [ 'android.permission.RECEIVE_BOOT_COMPLETED', 'android.permission.SCHEDULE_EXACT_ALARM', diff --git a/apps/mobile-shell/src/mobileShellWebViewPolicy.test.ts b/apps/mobile-shell/src/mobileShellWebViewPolicy.test.ts new file mode 100644 index 000000000..ec86e29c7 --- /dev/null +++ b/apps/mobile-shell/src/mobileShellWebViewPolicy.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from 'vitest'; + +import { BLOCK_WEBVIEW_DOWNLOAD_SCRIPT } from './mobileShellWebViewPolicy'; + +describe('BLOCK_WEBVIEW_DOWNLOAD_SCRIPT', () => { + test('阻断 WebView 内的网页下载链接', () => { + expect(BLOCK_WEBVIEW_DOWNLOAD_SCRIPT).toContain( + "element.tagName === 'A'", + ); + expect(BLOCK_WEBVIEW_DOWNLOAD_SCRIPT).toContain( + "element.hasAttribute('download')", + ); + expect(BLOCK_WEBVIEW_DOWNLOAD_SCRIPT).toContain('event.preventDefault()'); + expect(BLOCK_WEBVIEW_DOWNLOAD_SCRIPT.trim()).toMatch(/true;$/); + }); +}); diff --git a/apps/mobile-shell/src/mobileShellWebViewPolicy.ts b/apps/mobile-shell/src/mobileShellWebViewPolicy.ts new file mode 100644 index 000000000..de76082b5 --- /dev/null +++ b/apps/mobile-shell/src/mobileShellWebViewPolicy.ts @@ -0,0 +1,13 @@ +export const BLOCK_WEBVIEW_DOWNLOAD_SCRIPT = ` +document.addEventListener('click', function(event) { + var element = event.target; + while (element && element !== document) { + if (element.tagName === 'A' && element.hasAttribute('download')) { + event.preventDefault(); + return false; + } + element = element.parentNode; + } +}, true); +true; +`; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 873aac03b..2b17ce5d3 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -75,6 +75,7 @@ - 2026-06-18 移动壳麦克风权限禁用:Expo 移动壳的 `expo-image-picker` 插件必须保持 `microphonePermission=false`,Android 包配置必须通过 `android.blockedPermissions` 显式移除 `android.permission.RECORD_AUDIO`,且不得在 `android.permissions` 中重新声明;移动壳现阶段没有录音、后台音频采集或远程语音 SDK,音频导入只走系统文档选择器。配置检查会拒绝麦克风权限拦截缺失或被反向加入。 - 2026-06-18 移动壳 Android 自动备份关闭:Expo 移动壳必须保持 `android.allowBackup=false`,避免 WebView cookie、localStorage、缓存文件和宿主文件导入导出中间态进入 Google Drive 自动备份 / 恢复链路;正式业务事实仍以后端账号、作品、钱包和草稿状态为准。配置检查会拒绝恢复 Android 默认允许备份的包配置。 - 2026-06-18 移动壳 WebView 安全开关:Expo 移动壳 WebView 必须显式禁用 JS 自动开窗、多窗口、文件访问、file URL 跨源访问、HTTPS 混合内容、第三方 Cookie、共享 Cookie 和 WebView 远程调试;同源主站页面才能留在带 HostBridge 的 WebView 内,外链只通过受控协议离开容器交给系统。配置检查和移动壳导航测试会拒绝这些边界被放宽。 +- 2026-06-18 移动壳 WebView 默认下载边界:Expo WebView 内网页自动下载和 `` 直接落盘默认关闭;壳层注入脚本阻断 download 链接,iOS `onFileDownload` 只丢弃不落盘,Android 包配置通过 `blockedPermissions` 移除外部存储读写、管理外部存储和请求安装包权限。移动端文本、图片、音频保存只能通过 `file.exportText`、`file.exportImage`、`file.exportAudio` 等 HostBridge 受控导出能力进入系统分享 / 保存面板。 - 2026-06-18 移动壳 HostBridge 消息来源校验:Expo 移动壳 `onMessage` 必须根据 `event.nativeEvent.url` 校验消息来源,只有同源主站页面能进入 `handleMobileHostBridgeMessage`;`about:blank`、外域、协议降级和危险协议页面消息直接丢弃,不返回宿主能力错误细节。该规则与 WebView 导航留壳规则共用同源判断,配置检查和移动壳导航测试会拒绝移除。 - 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。 - 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index b8b4579e8..3290143e3 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -240,6 +240,7 @@ GameBridge 禁止: - Tauri 主 WebView 禁止默认下载落盘;桌面文件保存只能通过受控 HostBridge 导出能力进入系统保存对话框。 - Tauri 主 WebView 禁止默认打开 DevTools;不得通过配置或 Cargo feature 为分发壳启用浏览器检查器。 - RN WebView 禁止打开任意 URL 后仍保留完整 HostBridge;跳外链只允许 `http:`、`https:`、`mailto:`、`tel:`,并使用系统浏览器或降级能力,危险协议直接阻断。 +- RN WebView 禁止网页自动下载和 `` 直接落盘;移动端文件保存只能通过 `file.exportText`、`file.exportImage`、`file.exportAudio` 等受控 HostBridge method 进入系统分享 / 保存面板。 - Expo 移动壳的通知能力只覆盖即时本地通知;Android 包配置必须阻断重启后通知恢复和精确定时权限,前端代码不得注册 Expo push token、远程推送监听或通知响应跳转流程。 - AI sandbox iframe 必须使用独立 CSP、`sandbox` 属性和单独 GameBridge allowlist。 @@ -361,6 +362,8 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 WebView 原生安全开关显式收紧。`react-native-webview` 只加载同源主站入口,保留 JS 和 DOM storage 以运行现有 H5,但禁用 JS 自动开窗、多窗口、文件访问、file URL 跨源访问、HTTPS 页面加载 HTTP 混合内容、第三方 Cookie、共享 Cookie 和 WebView 远程调试;外链继续只允许 `http:`、`https:`、`mailto:`、`tel:` 离开 WebView 交给系统。`apps/mobile-shell/scripts/check-config.mjs` 和 `mobileShellNavigation.test.ts` 会覆盖这些壳边界,避免后续为单个页面调试把完整 HostBridge 暴露给外域页面。 +2026-06-18 追加:移动壳 WebView 默认下载路径显式关闭。壳层在 WebView 注入脚本中阻断 `` 点击,iOS `onFileDownload` 事件只丢弃不落盘,Android 包配置阻断外部存储读写、管理外部存储和请求安装包权限;H5 文本、图片、音频保存继续只能走 `file.exportText`、`file.exportImage`、`file.exportAudio` 的受控 HostBridge 导出能力。 + 2026-06-18 追加:移动壳 HostBridge 消息入口增加来源校验。`onMessage` 不只依赖导航拦截和 `originWhitelist`,还会读取 `event.nativeEvent.url`,只有同源主站页面才能进入 `handleMobileHostBridgeMessage`;`about:blank`、外域 URL、协议降级或危险协议页面发来的消息全部丢弃,不返回 HostBridge 错误细节。该校验与 `navigation.openNativePage` 共用同源规则,防止历史中间页或异常页面在带完整 HostBridge 的 WebView 中发起宿主能力请求。 ### Phase 4:宿主能力扩展