diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 20c5e8cd6..af942709c 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1265,7 +1265,9 @@ for (const snippet of [ } for (const snippet of [ + 'normalizeHostBridgeExternalUrl', 'resolveMobileShellWebViewUrl', + 'return normalizeHostBridgeExternalUrl(rawUrl)', 'shouldOpenInMobileShellWebView(rawUrl, allowedOrigin)', 'new URL(rawUrl, allowedOrigin).toString()', ]) { @@ -1274,6 +1276,17 @@ for (const snippet of [ } } +if ( + navigationSource.includes('javascript:') || + navigationSource.includes('mailto:') || + navigationSource.includes('tel:+') || + navigationSource.includes("protocol === '") +) { + throw new Error( + 'mobile shell WebView external protocol policy must use shared HostBridge normalizer', + ); +} + if (shellAppSource.includes('127.0.0.1:3000')) { throw new Error( 'mobile shell ShellApp must not hard-code localhost as the default H5 URL', diff --git a/apps/mobile-shell/src/shell/navigation.test.ts b/apps/mobile-shell/src/shell/navigation.test.ts index 731b5ec9a..335bfd6f8 100644 --- a/apps/mobile-shell/src/shell/navigation.test.ts +++ b/apps/mobile-shell/src/shell/navigation.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest'; +import { HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS } from '../../../../packages/shared/src/contracts/hostBridge'; import { resolveMobileShellExternalUrl, resolveMobileShellWebViewUrl, @@ -52,15 +53,16 @@ describe('shouldOpenInMobileShellWebView', () => { }); test('只有允许协议能交给系统外部应用打开', () => { - expect(resolveMobileShellExternalUrl(' https://example.com/path ')).toBe( - 'https://example.com/path', - ); - expect(resolveMobileShellExternalUrl('mailto:hi@example.com')).toBe( - 'mailto:hi@example.com', - ); - expect(resolveMobileShellExternalUrl('tel:+12345678')).toBe( - 'tel:+12345678', - ); + for (const protocol of HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS) { + const url = + protocol === 'mailto:' + ? 'mailto:hi@example.com' + : protocol === 'tel:' + ? 'tel:+12345678' + : `${protocol}//example.com/path`; + expect(resolveMobileShellExternalUrl(` ${url} `)).toBe(url); + } + expect(resolveMobileShellExternalUrl('javascript:alert(1)')).toBeNull(); expect(resolveMobileShellExternalUrl('file:///etc/passwd')).toBeNull(); expect(resolveMobileShellExternalUrl('/relative/path')).toBeNull(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e9bd114d0..8be4ff178 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -24,6 +24,7 @@ - 2026-06-18 外链接入:H5 新增 `openHostExternalUrl()` facade,`native_app` 下会把外链归一化为允许协议的绝对 URL 后请求 `app.openExternalUrl`;ICP备案号和 RPG 资产调试原图入口已优先走宿主系统浏览器,普通浏览器和小程序保留原 `` 行为,宿主不可用或拒绝时回退浏览器外链。 - 2026-06-18 外链协议白名单门禁:`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS` 是 `app.openExternalUrl` 唯一协议来源,当前只允许 `http:`、`https:`、`mailto:`、`tel:`;Expo 直接复用共享归一化逻辑,Tauri Rust 侧必须用 URL parser 镜像同一清单,根级 `npm run check:native-shells` 会拒绝共享契约与桌面壳协议清单漂移。 - 2026-06-18 移动壳 WebView 导航收紧:Expo WebView 自身拦截外域导航时复用 HostBridge 外链协议白名单,只把 `http:`、`https:`、`mailto:`、`tel:` 交给 `Linking.openURL`,`javascript:`、`file:`、相对异常路径等危险目标直接阻断,避免离开同源主站后仍保留完整 HostBridge。 +- 2026-06-19 移动壳 WebView 外链协议共源:`apps/mobile-shell/src/shell/navigation.ts` 的 WebView 外链离壳判断必须调用共享 `normalizeHostBridgeExternalUrl`,不得在 shell 层另写协议判断;`apps/mobile-shell/scripts/check-config.mjs` 会拒绝重新硬编码 `mailto:` / `tel:` / `javascript:` 等协议分支,`navigation.test.ts` 用 `HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS` 反查当前允许协议。 - 2026-06-19 移动壳系统分享 URL 边界:Expo `share.open` 调用 React Native 系统分享面板前,只允许把 `url`、`href`、`path`、`targetPath` 和 `work` 归一为 `https://app.genarrative.world` 同源公开 URL;外域、协议相对 URL、`javascript:` 等危险目标必须返回 `invalid_request`,且显式非法 payload 不得回退到之前缓存的 `share.setTarget` 目标。分享实现复用移动壳入口 URL 的生产主站 origin,配置检查会拒绝重新声明同值 origin 或移除协议相对 URL 拦截。 - 2026-06-19 桌面壳系统分享 URL 边界:Tauri `share.open` 写入系统剪贴板前同样只允许把 `url`、`href`、`path`、`targetPath` 和 `work` 归一为 `https://app.genarrative.world` 同源公开 URL;外域、协议相对 URL、`javascript:` 等危险目标必须返回 `invalid_request`,且显式非法 payload 不得回退到之前缓存的 `share.setTarget` 目标。桌面壳配置检查会拒绝移除同源分享 URL 归一和协议相对 URL 拦截。 - 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。