WebView 的默认导航策略两个方向都错。放任不管,每个链接——包括银行的支付页、还有钓鱼仿冒页——都在你自己的 WebView 里打开,把你的 Referer、会话 Cookie,以及真实披露过的 CVE 里写在 URL 上的鉴权令牌一并泄露。动手改又会在用户第一次点手机号时抛出 ERR_UNKNOWN_URL_SCHEME 崩溃。而默认没有宿主白名单,一次未设防的桥接调用就成了静默账号接管。宿主自有 runtime 按链接、按来源决定什么该出去、什么该留下。

默认就是「什么都在这里面打开」

默认情况下,WebView 会把每个 https 目标都在内部加载。点一下合作方的商城页、银行跳转页,甚至一篇外部新闻,都在你 App 自己的外壳里渲染。方便,直到它变成负债:目标站点现在能拿到你的 Referer 响应头、你拼在 URL 上的任何令牌,以及你的 WebView 写在 User-Agent 里的那个稳定用户 ID。2026 年 Mira Android 应用就被披露了这件事(CVE-2026-66832,CVSS 6.5)——它把用户实时的会话令牌作为查询参数拼进目标 URL,把一个稳定的用户 ID 塞进 WebView 的 User-Agent,两者都被传给第三方站点、Referer 日志,以及 WebView 内执行的任何脚本。这是账号接管,不是纸面假设。

改得不好,就崩溃

常见的反应是重写 shouldOverrideUrlLoading,把非 http 的 scheme 转交给系统。返回值写反了,啥也不发生;漏掉 tel:、mailto:、sms:、geo: 或 intent://,用户就看到 ERR_UNKNOWN_URL_SCHEME——WebView 根本渲染不了它不认识的 scheme。Android 7+(API 24)强制要求新方法签名,返回值写错就抛 ActivityNotFoundException 直接崩。Android 11+ 又加了包可见性规则,跳转到另一个 App 的深链若未声明就会静默失败。修复是真的,但琐碎,而且它活在 Web 团队并不拥有的原生代码里。

没有白名单,桥就是敞开的大门

更深的失败是结构性的。导航策略与 JS 桥,是 Web 内容与原生能力之间的两道闸门;而在一份公开审计里(SilentGrid),两道都不在:桥把实时鉴权令牌回给页面上任意 JavaScript,WKWebView 的 decidePolicyFor 又对任何 https 来源放行、毫无主机校验。一个精心构造的深链就足以加载攻击页、调用令牌处理器、把会话窃走——一次点击,受害者毫无察觉。反向 Tabnabbing 又添一把火:没有 rel="noopener" 打开的页面能悄悄把你的 opener 重定向到钓鱼仿冒页,而 WebView 并不套用浏览器的那层保护默认。

runtime 解法

宿主自有 runtime 把导航边界变成显式且被强制执行的规则。它维护一份宿主白名单:第一方域名在 runtime 内加载,已知的 App scheme(tel:、mailto:、market:)转交系统并带安全兜底,其余一律要么外部浏览器打开、要么拦截。JS 桥按来源校验、最小权限,所以一个来自不可信 catalog 条目的页面够不到凭据处理器。该出去的链接,出去;该留下的令牌,留下。

维度WebView宿主自有 runtime
默认行为每个 https 都在 WebView 内打开第一方在内,其余出去或拦截
外部链接Referer、URL 令牌、UA ID 泄露给第三方宿主白名单管控每个来源
自定义 scheme未处理即 ERR_UNKNOWN_URL_SCHEME 崩溃转交系统并带安全兜底
JS 桥常无来源校验按来源校验、最小权限
反向 Tabnabbing不强制 noopener每个新窗口强制开启

本系列的主线始终是「控制权」。WebView 把一条两个方向都不安全的默认导航边界、一个谁调用都信任的桥,塞给你。runtime 把边界变成一条由你书写的策略——按链接、按来源、按 catalog 条目。对一套承载别人页面在你的 App 里交易的 catalog 来说,这就是「受控店门」与「敞开大门」的差距。

参考来源