Skip to content

Security

Electron’s security model requires that sensitive BrowserWindow options — particularly webPreferences — are never controllable from renderer code. This library enforces that boundary.

The library enforces a non-negotiable baseline on every child window, regardless of consumer config:

  • nodeIntegration: false
  • contextIsolation: true
  • sandbox: true (default — can be explicitly set to false via defaultWindowOptions.webPreferences.sandbox)

Beyond this floor, webPreferences is only configurable via setupWindowManager in the main process. Renderer-supplied props cannot reach webPreferences — they’re filtered through an allowlist before touching BrowserWindow.

  • webPreferences — main-process-only. Renderer props can’t override nodeIntegration, contextIsolation, preload, etc.
  • Arbitrary URLs — child windows load only about:blank. Other URLs rejected by setWindowOpenHandler.

Two mechanisms restrict which renderer origins can use the IPC surface:

Runtime — works whether or not you bundle your main process:

setupWindowManager({
allowedOrigins: ["app://main", "file://"],
});

Validated per IPC call against the parent renderer’s top-frame origin. Unset → permissive (dev warning). ["*"] → explicitly allow all.

Build-time — if you bundle both main and preload, define a constant in your bundler:

// vite / esbuild / webpack DefinePlugin — for BOTH main and preload builds
define: {
__ELECTRON_WINDOW_ALLOWED_ORIGINS__: JSON.stringify(["app://main", "file://"]),
}

This additionally gates the preload: on a wrong origin, window.electron_window is never exposed. The check runs in the generated IPC validator against event.senderFrame.url (the correct frame).

A renderer can only mutate windows it registered. If you setupForWindow on multiple parent windows, each is isolated — WebContents B cannot DestroyWindow, UpdateWindow, or WindowAction on WebContents A’s child windows, even with knowledge of the window ID.

setWindowOpenHandler also verifies ownership: a different WebContents calling window.open("about:blank", stolenId) is denied.

Child windows created by <Window> have a blanket setWindowOpenHandler(() => ({ action: "deny" })). Any window.open() from inside a child window’s document returns null.

This doesn’t affect nested <Window> components — they call window.open() from the parent renderer’s context (React runs in the parent; only the DOM is portaled into the child). But third-party libraries that call element.ownerDocument.defaultView.open(...) from inside a child window’s DOM will be blocked.

A dev warning fires when this happens, naming the denied URL.

Window creation is rate-limited and pending registrations have a TTL to prevent orphaned-registration DoS:

OptionDefaultDescription
maxPendingWindows100Max windows awaiting creation
maxWindows50Max total open windows
Pending TTL10sStale pending entries auto-evicted

The 10-second TTL is typically invisible (the RegisterWindowwindow.open handshake takes microseconds). If debugging with a breakpoint between register and open, the entry may expire — reopen the window.

IDs are generated via crypto.randomUUID() (falling back to crypto.getRandomValues in non-secure contexts). 122 bits of entropy makes guessing infeasible — cross-WebContents attacks require observation (DevTools, leaked state), not brute force.

ThreatMitigation
Renderer overrides webPreferencesMain-process-only + enforced floor (nodeIntegration: false, etc.)
Arbitrary URL navigationabout:blank only; others denied
Unexpected IPC propsAllowlist filtering before BrowserWindow
IPC from iframesMain-frame-only validator in the IPC layer — rejected before handlers run
Wrong-origin rendererallowedOrigins (runtime, per-call) or __ELECTRON_WINDOW_ALLOWED_ORIGINS__
Cross-renderer window controlPer-WebContents ownership checks on every mutation
Runaway window creationmaxPendingWindows, maxWindows, 10s pending TTL
Guessable window IDscrypto.randomUUID() — 122 bits of entropy