RSB 内置浏览器销毁 popup 的浏览上下文,依赖 window.open 返回句柄的 OAuth/SSO 弹窗流程无法完成
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
**客户端版本**: 0.1.28
**反馈类型**: bug
---
> 域名/凭据未写入本文;下文用本地最小复现承载论证(真实场景为企业内网 SSO)。
## 现象
RSB 内置浏览器里,`window.open()` 拿到的句柄**在弹窗首次导航后即失效**:`closed` 变 `true`、`location` 不再指向弹窗(实测读回 opener 自身 URL)、`close()` 无效、跨窗口 `document.write` 无处可写;弹窗内容却被另起一个 tab 正常加载——**内容在,身份没了**。依赖该句柄的流程因此中断:企业 SSO(CAS + 第三方 IdP 扫码)授权后不回跳、反复要求授权、授权 tab 停在回调页不自关;同流程在系统 Chrome 正常。
## 复现步骤
本地 HTTP 服务托管两个同源静态页(`file://` 不适用)。`callback.html` **刻意无脚本**,只作携带 `?code=` 的 URL 载体。`opener.html` 按钮回调:
```js
var w = window.open('', 'popup', 'width=520,height=420');
w.document.open(); w.document.write('
by opener
'); w.document.close();w.location.replace(location.href.replace('opener.html','callback.html') + '?code=DEMO');
setInterval(function () {
var c=null,h=null; try{c=w.closed}catch(e){} try{h=w.location.href}catch(e){}
log('closed=' + c + ' href=' + h);
if (h && h.indexOf('code=') > -1) { w.close(); }
}, 250);
```
在 RSB 内置浏览器打开 `http://127.0.0.1:/opener.html` 后点按钮。
## 期望行为
`closed` 始终 `false`,`href` 在若干次轮询内变为 `…/callback.html?code=DEMO`,随后读出 code、`close()` 后 `closed=true`(系统 Chrome 即此行为)。
## 实际行为
纯 `origin/main` 构建实测:
```
1) open("") => Window
2) cross-window write => ok
3) opener navigates popup => …/callback.html?code=DEMO
4) poll#1 closed=true href=…/opener.html
…每次轮询同样,直到超时,始终读不到 ?code=DEMO
```
前两步(中转窗存活期间)正常;**导航后句柄立即退化**——`closed` 变 `true`,`location.href` 读到 **opener 自身 URL** 而非弹窗。
## 根因
`apps/desktop/src/main/webview-security.ts`:guest 的 `setWindowOpenHandler` 对 `about:blank` 形态放行一个隐藏 `BrowserWindow`(“中转窗”),opener 由此拿到真实句柄;随后 `installDeferredPopupRouter` 的 `route()` 在首个 http(s) 导航时 `popupWindow.close()`(约 :518 / :530),把 URL 交给 renderer **另起 `` tab**。URL 相同但**身份已替换**:opener 的句柄指向已销毁窗口,承载内容的 tab 与 opener 无句柄关系。相关时限 `DEFERRED_POPUP_ROUTE_TIMEOUT_MS = 10_000`(:268)。
真实 SSO 中句柄承担三事(反混淆等价描述):opener 用 `.location.replace()` 导航弹窗到 IdP、完成后 `.close()`、跨窗口 `document.write` 写过渡页。该流程的**登录完成信号**另走服务端长轮询 + 同源 iframe→父页 `postMessage`、不经 popup,故本 issue 不主张“句柄失效→登录必然不完成”的单一因果链,只主张句柄能力面缺失致多环节退化;企业链路未逐环取证,以最小复现为准。
**已排除**:popup→opener 的 `postMessage`(回调页零脚本、完成信号也不经 popup);cookie/会话(两侧 session cookie 实测一致,且强制同一 partition,约 :446);UA(覆盖为纯 Chrome 后现象不变);服务端(同服务端在系统 Chrome 跑通)。另:本地试过“主世界仿真句柄”(约 900 行)不可行——跨窗口 `document.write` 无法仿真、`location.href` 须同步返回而权威状态在 main。
## 建议方向
本质是**呈现层决策与浏览上下文身份被耦合**:“内容画侧栏、不弹 OS 窗口”合理,但“造真窗口→捕获 URL→`close()`→另起 tab”把身份一并丢了。应解耦:让 popup 的 WebContents **从诞生起就活在侧栏里**,而不是先造再销毁、把内容搬走。
Electron 41.2.0 已有能力:`setWindowOpenHandler` 返回 `{ action:'allow', createWindow: (options) => WebContents }`(官方示例正是“让新窗口以 BrowserView 形式打开”),由宿主自建 WebContents、用 `WebContentsView` 承载并 `contentView.addChildView`,bounds 经 IPC 推送。**预期**原生提供 opener 关系与 WindowProxy 语义,但**尚未验证**——建议先 spike(断言 `closed`、`document.write`、同源 `location.replace` 后 `pathname` 可读、`close()` 均生效)再推进。
两处漏改会引入更严重问题:
1. **崩溃豁免(最高优先)**:`lifecycle.ts` 的 `render-process-gone` 只豁免 ghost sandbox 与 `getType()==='webview'`,其余 `beginShutdown` + `app.exit(1)`。新 popup WebContents 不属这两类,**漏改会让一个网页弹窗崩溃拖垮整个 App**(与该处注释“guest 崩溃不应退 Workbench”同源)。
2. **绕过加固**:`installWebviewHardener` 全挂 `will-attach-webview`/`did-attach-webview`,新 WebContents 默认拿不到 webPreferences 强制项、`partition`、嵌套 `window.open` handler、`before-input-event` 拦截、guest preload,须显式继承;且用 `createWindow` 后 **`did-create-window` 不再触发**。
另需评估:`outlivesOpener` 与实例池 LRU(K=5,opener 被汰汰会连带 popup)、native view 与 overlay 层叠、三态侧栏切换的 reparent、两套 tab 载体的适配成本。另一处独立观察:`isInitialBlankPopupUrl`(约 :450)只匹配 `about:blank`/`about:blank#blocked`,若 Electron 对 `window.open('')` 上报空串则落到末尾 `deny`(约 :549 / :648)返回 `null`,而规范要求创建 about:blank 上下文;**未取实证**,与根因独立,单修不解决本 issue。
## 影响与相关
受影响:任何依赖 `window.open` 返回句柄的流程(读弹窗 URL、写弹窗内容、由 opener 导航/关闭),OAuth/SSO 弹窗最常见;系统浏览器与自动化浏览器不受影响。后果是无声中断、无提示可自查、空白授权 tab 堆积。
已在 **0.1.28 与当日主干 `6238a82c`** 复核,上述代码事实**均未变化**;行号基于 `origin/main` 可能漂移。侧边栏为内嵌主窗形态、tab 数少于池上限。
相关(同根不同断点,均为已合并 PR **#742** 的后续):**#1094** 修 main→renderer 投递可靠性(消息丢弃 → tab 未创建),本 issue 是 tab 已创建但句柄失效;**#186** 是登录 Cindy 账号本身、走 `shell.openExternal`。
---
**版本区域**: CN
**OS**: darwin x64 (24.6.0)
**界面语言**: zh-CN
Contributor guide
Assessment
This issue has not been assessed yet.