关闭移动壳网页下载路径
移动壳 WebView 注入下载阻断脚本并丢弃 iOS 文件下载事件。 移动壳 Android 包配置阻断外部存储和请求安装包权限。 移动壳配置检查和 Expo public config smoke 锁定下载边界。 新增移动壳 WebView 下载阻断策略测试。 更新宿主壳方案和共享决策日志记录移动端下载只能走 HostBridge 导出。
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;$/);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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 内网页自动下载和 `<a download>` 直接落盘默认关闭;壳层注入脚本阻断 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 不允许任意远端页面调用桌面命令。
|
||||
|
||||
@@ -240,6 +240,7 @@ GameBridge 禁止:
|
||||
- Tauri 主 WebView 禁止默认下载落盘;桌面文件保存只能通过受控 HostBridge 导出能力进入系统保存对话框。
|
||||
- Tauri 主 WebView 禁止默认打开 DevTools;不得通过配置或 Cargo feature 为分发壳启用浏览器检查器。
|
||||
- RN WebView 禁止打开任意 URL 后仍保留完整 HostBridge;跳外链只允许 `http:`、`https:`、`mailto:`、`tel:`,并使用系统浏览器或降级能力,危险协议直接阻断。
|
||||
- RN WebView 禁止网页自动下载和 `<a download>` 直接落盘;移动端文件保存只能通过 `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 注入脚本中阻断 `<a download>` 点击,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:宿主能力扩展
|
||||
|
||||
Reference in New Issue
Block a user