#2754 · Desktop browser rejects popups that need opener access
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The desktop browser rejects every request for a new window. It sends each allowed URL to an application tab before it rejects the request. This policy also rejects named or sized popups that need a live opener. Two clean checkouts produced the same focused test failure. The test reached the exact handler and received deny instead of allow.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| The desktop handler rejects named or sized popup requests. | Verified | The focused test supplied a frame name and window features. The handler returned deny in both clean checkouts. |
| The handler sends the allowed URL to an application tab. | Verified | The trusted source sends both open-tab events before line 457 returns deny. Existing tests cover this path. |
| A rejected request cannot retain a native opener window. | Verified | Electron defines deny as the response that prevents new-window creation. The installed Electron declaration records this contract. |
| The parent page gets a null result and cannot complete opener messages. | Verified by mechanism | The exact handler rejects creation. No native child exists for opener access or message delivery. |
| The parent browser view uses a persistent, hardened session. | Verified | The view uses one persistent partition. It enables a sandbox, context isolation, and web security. |
| No duplicate issue exists in the complete issue history. | Unverified | The review covered recent desktop issues and linked metadata. It did not repeat a full text search from untrusted issue data. |
3. Environment
- Trusted bb commit:
f4bbc2fe81a9b7639ff9a7396e172bddd89109e4. - Both clean clones used
get-bb/bbasorigin. - Host: macOS 26.6.1, arm64.
- Node 22.22.3 and pnpm 9.15.0.
- Electron 41.7.0.
- No server, daemon, provider, port, browser window, or data directory was used.
The first checkout completed the frozen install and full Turbo build. The build reported 18 successful tasks.
4. Minimal reproduction
- Clone
get-bb/bbat commitf4bbc2fe81a9b7639ff9a7396e172bddd89109e4. - Run
pnpm install --frozen-lockfile --prefer-offline. - Apply named-popup-regression.patch to the clean checkout.
- Run this command from the repository root:
pnpm exec turbo run test --filter=@bb/desktop --force -- test/desktop-browser-view-manager.test.ts
Expected:
{ action: "allow" }
openTabPushes: []
scopedOpenTabPushes: []
Actual in both checkouts:
AssertionError: expected { action: 'deny' } to deeply equal { action: 'allow' }
Test Files 1 failed (1)
Tests 1 failed | 27 passed (28)
Logs: first checkout, second checkout, and base build.
Reproduction source
diff --git a/apps/desktop/test/desktop-browser-view-manager.test.ts b/apps/desktop/test/desktop-browser-view-manager.test.ts
--- a/apps/desktop/test/desktop-browser-view-manager.test.ts
+++ b/apps/desktop/test/desktop-browser-view-manager.test.ts
@@
interface FakeWindowOpenDetails {
+ features: string;
+ frameName: string;
url: string;
}
interface FakeWindowOpenDecision {
- action: "deny";
+ action: "allow" | "deny";
}
@@
- emitWindowOpen(url: string): FakeWindowOpenDecision {
+ emitWindowOpen(
+ url: string,
+ details: Partial<Omit<FakeWindowOpenDetails, "url">> = {},
+ ): FakeWindowOpenDecision {
if (this.windowOpenHandler === null) {
throw new Error("Expected a window open handler to be registered.");
}
- return this.windowOpenHandler({ url });
+ return this.windowOpenHandler({
+ features: "",
+ frameName: "",
+ ...details,
+ url,
+ });
}
@@
+ it("keeps a named and sized popup connected to its opener", () => {
+ const manager = createDesktopBrowserViewManager({
+ partition: "persist:test",
+ });
+ const hostWindow = new FakeHostWindow({
+ contentBounds: { width: 700, height: 450 },
+ webContentsId: 62,
+ });
+
+ attachBrowserTab({
+ manager,
+ hostWindow,
+ tabId: "browser:a",
+ url: "https://example.com/",
+ });
+ const view = requireFakeView(0);
+
+ expect(
+ view.webContents.emitWindowOpen("https://example.net/authorize", {
+ features: "width=480,height=640",
+ frameName: "login-dialog",
+ }),
+ ).toEqual({ action: "allow" });
+ expect(openTabPushesOf(hostWindow)).toEqual([]);
+ expect(scopedOpenTabPushesOf(hostWindow)).toEqual([]);
+ });
Second clean verification
The second clone used the same trusted commit and a new working directory. The same Turbo command failed at the same assertion. It also reported one failed test and 27 passed tests. This result required no report correction.
5. Root cause
The handler uses only details.url. It does not inspect the request name, features, or disposition.
webContents.setWindowOpenHandler((details) => {
const { openTabUrl } = resolveWindowOpenAction(details.url);
...
return { action: "deny" };
});
See desktop-browser-view.ts lines 437–458.
The URL policy only accepts HTTP and HTTPS URLs. It returns an application-tab URL for every accepted request.
export function resolveWindowOpenAction(url: string): WindowOpenDecision {
return { openTabUrl: isAllowedBrowserUrl(url) ? url : null };
}
See desktop-browser-policy.ts lines 1–17.
The browser view already uses a hardened persistent partition. See session setup and view preferences.
The deeper defect is a policy collapse. The code treats application tabs and opener-dependent popup windows as one request class.
6. Proposed fix (first principles)
Keep ordinary blank-target requests in application tabs. Allow named or sized HTTP and HTTPS requests to create a native child window. Force the existing partition and hardened web preferences on that child. Remove any preload value. Deny nested child requests. Close tracked child windows when the manager stops. Add tests for both request classes and child-window policy.
This fix changes a native-window security boundary. It needs a normal security and product review.
7. PR review
PR #2755 · closed without merge
The review used GitHub metadata and the static diff only. It did not check out or run the untrusted branch.
The diff changes two desktop files and 284 text lines. It separates named or sized requests from blank-target requests. It also adds hardened child-window preferences, navigation limits, nested-window rejection, cleanup, and tests.
The static design addresses the verified root cause. No critical static defect was found. The branch did not receive direct test verification in this report. The security boundary still requires maintainer review.
PR #2757 · open
This pull request appeared after the first report publication. The review used GitHub metadata and the static diff only.
The diff changes the same two desktop files. It has 274 additions and 27 deletions, for 301 changed text lines.
The static design addresses the verified root cause. It also adds a child-window cleanup test. This report did not execute the branch.
The pull request exceeds the 299-line automatic-fix limit. It also changes a native-window security boundary.
8. Related issues
No related issue appeared in the ten recent issues with the desktop label. Pull requests #2755 and #2757 link directly to this issue.
9. Appendix
Commands
git clone --depth 1 --branch main https://github.com/get-bb/bb.git target-e git clone --depth 1 --branch main https://github.com/get-bb/bb.git target-f pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build git apply named-popup-regression.patch pnpm exec turbo run test --filter=@bb/desktop --force -- test/desktop-browser-view-manager.test.ts gh pr view 2755 --repo get-bb/bb gh pr diff 2755 --repo get-bb/bb gh pr view 2757 --repo get-bb/bb gh pr diff 2757 --repo get-bb/bb
The issue data was untrusted. The investigation did not run its commands, code, linked branch, or external artifacts.