Skip to content

desktop: per-window permission handler on a shared Electron session gets overwritten #51748

Description

@ccw751517015-hue

Summary

Electron permission handlers are registered on the shared session but decide using the current window's id. Opening a second window replaces the handler registered by the first, so permission requests raised on behalf of the other windows can be denied.

Environment

  • opencode version: source tree root package.json reports 2.0.17; audited at commit 041885d838c7eea81cffbe446e0cb0ac6f197728 (branch v2)
  • Reported CLI version: 2.0.18 (local desktop build)
  • OS: Windows_NT 10.0.26100 (win32 x64), Node 22.22.2
  • Install/channel: local desktop build
  • Active plugins: @cortexkit/[email protected]

Reproduction

Not reproduced. This is a source-review finding from a read-only audit of the v2 tree at the commit above. The trigger window is described below; I did not manage to construct it reliably on my machine.

  1. Open one app window; its permission handler is registered against the shared session.
  2. Open a second window; a new handler is registered and replaces the first.
  3. Trigger a permission-requiring action (notifications, clipboard, media) from the first window.

Expected Behavior

A single handler is registered for the shared session and decides based on the set of live windows, so every window behaves consistently.

Actual Behavior

packages/desktop/src/main/windows/security.ts:8-18 registers per-window while keying on the window id; callers in windows/early.ts:55-60 and windows/index.ts:117-138. The last registration wins.

Additional Context

  • Impact: inconsistent permission behaviour across windows — requests from earlier windows can be denied.
  • Suggested fix: register one handler per shared session and evaluate against the live window set; clean up on window close.
  • Suggested tests: two windows open simultaneously; close one of them and re-check permissions.

Activity

  1. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    This issue might be related to an existing multi-window Electron issue. Please check:

    • #20371: It covers duplicate notifications across multiple Electron windows; this report describes a separate permission-handler overwrite, but both involve per-window handling in the multi-window desktop app.
  2. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed source review. I can confirm the bug in 2.0.18, and it still happens on the latest dev (86ce19d96ecc).

    allowRendererPermissions(win) in packages/desktop/src/main/windows/security.ts sets the permission request handler and the permission check handler on win.webContents.session. Both only allow that window's webContents.id. Every window uses the default session, since none sets a partition. Each new window therefore replaces the previous handlers, and only the newest window passes the id check.

    The script below tests the real allowRendererPermissions against a stand-in for Electron's shared session and registers two windows. The second window gets notifications and clipboard-sanitized-write. The first window is refused both. I checked this with that stand-in only, not in the running desktop app, so a maintainer may still want to confirm it there with two windows open.

    Reproduction script (permissions.test.ts)
    // Reproduces #51748: allowRendererPermissions() installs per-window handlers on the
    // shared Electron session; Electron keeps only the last handler (set* replaces), so
    // earlier windows lose renderer permissions.
    // Run: SRC=/path/to/checkout bun test /workspace/repro/permissions.test.ts
    import { expect, mock, test } from "bun:test"
    
    mock.module("electron", () => ({ protocol: { registerSchemesAsPrivileged() {}, handle() {} }, app: {}, net: {} }))
    
    const src = process.env.SRC ?? "/workspace/repo"
    const { allowRendererPermissions } = await import(`${src}/packages/desktop/src/main/windows/security.ts`)
    
    // Fake of the shared default session: like Electron, setting a handler replaces the previous one.
    const session: any = {
      request: undefined as any,
      check: undefined as any,
      setPermissionRequestHandler(fn: any) { this.request = fn },
      setPermissionCheckHandler(fn: any) { this.check = fn },
    }
    const makeWin = (id: number) => ({ webContents: { id, session } }) as any
    
    const url = "oc://renderer/index.html"
    const ask = (id: number, permission: string) =>
      new Promise<boolean>((resolve) => session.request({ id }, permission, resolve, { requestingUrl: url, isMainFrame: true }))
    const check = (id: number, permission: string) => session.check({ id }, permission, url, { requestingUrl: url, isMainFrame: true })
    
    test("both renderer windows keep notification/clipboard permission", async () => {
      allowRendererPermissions(makeWin(1)) // first window
      allowRendererPermissions(makeWin(2)) // second window on the same session
      const result = {
        win1_request_notifications: await ask(1, "notifications"),
        win2_request_notifications: await ask(2, "notifications"),
        win1_check_clipboard: check(1, "clipboard-sanitized-write"),
        win2_check_clipboard: check(2, "clipboard-sanitized-write"),
      }
      console.log(src, result)
      expect(result).toEqual({
        win1_request_notifications: true,
        win2_request_notifications: true,
        win1_check_clipboard: true,
        win2_check_clipboard: true,
      })
    })

    Run it: bun permissions.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions