Android 的 mediaCapturePermissionGrantType 空转:离线 WebView 的子 realm 仍可零提示取麦克风/摄像头(需冷更)
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
从 PR #1441(手机端 HTML 生成物进渲染态)拆出。**该 PR 已在 JS 层封住顶层 realm,本 issue 跟踪剩下的原生层缺口。**
## 问题
`` 在 iOS 上正确工作
(`RNCWebViewImpl.m` 的 `requestMediaCapturePermissionForOrigin` → `WKPermissionDecisionDeny`),
但 **Android 上完全空转**,有两处依据:
1. **new architecture:setter 是空函数**
`node_modules/react-native-webview/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java:427`
```java
public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable String value) {}
```
2. **old architecture:连这个 setter 都没有** —— `android/src/oldarch/.../RNCWebViewManager.java` 里
不存在同名方法,该 prop 在 old arch 下根本不进原生。
实际权限判定全在
`android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java:146`
的 `onPermissionRequest`,它**只看 app 级 OS 运行时权限**:
```java
if (ContextCompat.checkSelfPermission(context, androidPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(requestedResource);
}
// ...
if (requestedAndroidPermissions.isEmpty()) {
request.grant(grantedPermissions.toArray(new String[0])); // ← 同步 grant,连异步权限框都不走
return;
}
```
`apps/mobile/package.json` 里的 `expo-audio` / `expo-image-picker` 会把 `RECORD_AUDIO` / `CAMERA`
合并进 AndroidManifest。用户为语音输入或拍照附件授过权限之后(常见状态),该 WebView 里的
任意不可信 HTML 都能**零提示、零 UI 指示**拿到实时音视频流。
## PR #1441 已经做到哪一层
`apps/mobile/src/session/htmlPreviewCsp.ts` 的前导 guard(拼在 HTML 文本最前面,由**解析器**保证
先于任何作者脚本执行,零竞态)把 `mediaDevices` / `getUserMedia` / `webkitGetUserMedia` /
`mozGetUserMedia` 在**实例与原型两处**都 `defineProperty` 成 `undefined`
(`writable:false, configurable:false`),并封掉四类嵌入元素的 `contentWindow` / `contentDocument`。
**顶层 realm 因此已封住**:`getUserMedia` 不存在 → `onPermissionRequest` 根本不会被触发。
## 剩下的缺口(本 issue 要修的)
**子 browsing context**。不可信 HTML 里直接写一个无 `src` 的 iframe,解析期就同步得到一个
未加固的初始 `about:blank` realm,它有自己的 `Navigator.prototype`
(实测 `w.Navigator.prototype === window.Navigator.prototype` → `false`):
```html
window[0].navigator.mediaDevices.getUserMedia({audio:1,video:1})
```
`window[0]` **无法从 JS 层封掉** —— `Object.defineProperty(window,'0',…)` 在 WindowProxy 上直接抛
`TypeError: Failed to set an indexed property [0] on 'Window'`。iframe 也不需要脚本创建。
**所以这一格只能在原生层解决**,而原生层的好处是它不分 realm:一次 `request.deny()` 覆盖全部
browsing context。
## 建议修法
在 `dependency-patches/react-native-webview@13.16.1.patch` 里补 Android 语义,二者择一:
1. 让 `setMediaCapturePermissionGrantType` 真正落到 `RNCWebChromeClient`,并在 `onPermissionRequest`
开头按该值 `request.deny()`(需同时补 old arch 的 setter,否则 old arch 下仍空转);
2. 或给 `RNCWebChromeClient` 加一个「不可信来源」开关,命中时无条件 `request.deny()`。
**验收必须覆盖「app 权限已授予」这个场景** —— 那正是漏洞成立的前提;只测未授权状态会假通过。
同时覆盖子 realm(上面那段 iframe PoC)。
## ⚠️ 为什么不在 #1441 里做:冷更门
`dependency-patches/*.patch` 在 `package.json` 的 `pnpm.patchedDependencies` 里,patch 的是 Android
原生 Java → **进 runtime fingerprint、触发冷更**。按 `docs/dev-rules/mobile-development.md`
的冷更边界,这类改动必须由仓库指定把关人**针对冷更**明确确认才能合并,「不看改动大小,也不看
谁提的,提交者身份不构成例外」,而且要转 draft、退出自动合并路径。
**建议:搭下一次必须冷更的改动同一班车。** 冷更一次和两次对用户代价一样(都要重装/重下原生包),
攒着一起出最省。本 issue 不单独催冷更。
## 影响面与当前风险敞口
- 平台:**仅 Android**(iOS 的原生 deny 已生效,不分 realm)。
- 前提:用户已为 Cindy 授予 `RECORD_AUDIO` 或 `CAMERA`(为语音输入/拍照附件,常见)。
- 触发:打开一个恶意/被提示注入污染的 HTML 生成物预览,产物里含无 `src` iframe + 一行脚本。
- **外传是否可能**:CSP `connect-src 'none'` 被 about:blank 子 frame 继承(实测),所以
fetch/XHR 外传封住;但 **WebRTC 不受 CSP 各 `*-src` 管辖**,而 `webrtc 'block'` 实测在
Chrome 150 上 meta 与 HTTP 头两种下发**都不生效** → 子 realm 的 `RTCPeerConnection` 仍可用。
即「取到流」与「送出去」两步在 Android 子 realm 上都不能排除。
- 缓解现状:#1441 已把顶层封死,并在 `htmlPreviewCsp.ts` 注释里写明完整实测矩阵,
未夸大成「已封死」。
## 相关
- PR #1441 — 顶层 realm 的 JS 层封锁 + 实测残留矩阵
- 该 PR 里同一问题的两条 review:MagicLizi 的 P0、Greptile 的 P1(均已回复并说明归属本 issue)
Contributor guide
Research direction
Start with dependency-patches/react-native-webview@13.16.1.patch, the new- and old-architecture RNCWebViewManager.java setters, and RNCWebChromeClient.java:onPermissionRequest. Reproduce the iframe PoC with RECORD_AUDIO or CAMERA already granted, then verify both architectures deny the request for the mediaCapturePermissionGrantType behavior. Acceptance requires coverage of the granted-permission case and the child realm, with the change treated as a cold-update modification.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, java, react-native, typescript
- Domain
- mobile, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 42/100