Electron Desktop Security

SkillFiles & storage

Harden Electron: renderer trust boundary (nodeIntegration, contextIsolation, sandbox), build-time fuses, contextBridge and IPC allowlists, shell.openExternal, navigation guards, deep-link auth, safeStorage. Use when generating main-process code, a preload script, or custom-protocol handlers, when packaging a release, or when storing tokens in an Electron app.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Electron Desktop Security skill

What this skill tells your AI

The instructions your AI receives, as published by shieldnet-360/secure-vibe in skills/electron-security/SKILL.md and read by ahel’s review.

Rules (for AI agents)

ALWAYS

  • Treat the renderer as untrusted. Any renderer-side code execution — XSS in rendered content, a redirect, a deep link — must not be able to reach Node, the shell, the filesystem, or session tokens. Every other rule here follows from this one, and every IPC sink you expose is reachable from a compromised renderer.
  • Keep the renderer's process isolation at its defaults rather than restoring them: nodeIntegration: false, contextIsolation: true and sandbox: true have been Electron's defaults for several major versions, so the finding is the line that turns one off. Note the coupling: under sandbox: true a preload gets a polyfilled module subset, so a preload needing fs fails — move that work behind IPC into the main process, never drop the sandbox.
  • Set the packaging fuses on release builds: disable RunAsNode, EnableNodeCliInspectArguments and EnableNodeOptionsEnvironmentVariable. With RunAsNode on, an attacker runs your signed binary as a plain Node process and every renderer-side control above becomes irrelevant — a build-time setting no runtime hardening substitutes for.
  • Expose a minimal, typed API from the preload via contextBridge.exposeInMainWorld. Expose named functions only — never hand the renderer ipcRenderer, require, process, or whole modules. Do not add @electron/remote: it restores the main-process object access that was removed from core precisely because it defeats the boundary.
  • Validate every IPC argument in the main-process handler: type-check, bound, and allowlist. The renderer is an attacker-controlled input source.
  • Spawn child processes with execFile / spawn and an argument array, never exec with a shell string built from renderer input — that is command injection. Allowlist each argument (e.g. ^[A-Za-z0-9_-]+$).
  • Confine filesystem paths: path.resolve(base, input), then verify the result startsWith(base + path.sep). Reject absolute paths and .. segments. Concatenating a renderer-supplied path (${BASE}${filePath}) is path traversal and arbitrary file write.
  • Allowlist shell.openExternal to https: (and mailto: if needed) after parsing the URL. Reject file:, custom schemes, and anything else — an arbitrary or renderer-controlled URL here is a local-launch and RCE vector.
  • Add navigation guards: app.on('web-contents-created', …) with contents.on('will-navigate', …) and contents.setWindowOpenHandler(…) that deny by default against a strict origin allowlist. Keep webviewTag: false; where a <webview> is genuinely required, strip its preload and reset its options in will-attach-webview.
  • Remember the contextBridge surface is exposed to whatever origin the webContents currently holds — the preload re-runs on navigation and exposeInMainWorld does not re-check origin. One missing will-navigate guard lets an attacker origin inherit your entire IPC surface: that is how a stored hyperlink becomes 1-click RCE. Gating the preload on location is defense in depth, not the control.
  • Before attaching session tokens or cookies to an outbound request, verify the target host is on your own-API allowlist. Never attach credentials to a renderer-supplied URL — an XSS then exfiltrates the token.
  • Bind custom-protocol / deep-link auth to a one-time state / PKCE value the app generated and is waiting for; validate before storing any token. Accepting myapp://auth?refresh-token=… unvalidated is login CSRF and session fixation.
  • Store tokens with Electron safeStorage, not app-level crypto — and check safeStorage.isEncryptionAvailable() first: on Linux with no keyring the backend falls back to a fixed key, so the ciphertext is not confidential. Ship release builds code-signed, with ASAR integrity where the platform supports it (macOS and Windows).
  • Treat every server the app connects to — your own API, an auto-update channel, a telemetry endpoint, a shared multi-tenant backend — as potentially attacker-controlled. Never load a server-supplied URL into a window carrying your preload, and never feed a server response into an IPC sink (file path, shell argument, openExternal URL) without the validation you apply to renderer input.
  • Harden parsers consuming untrusted server or stream data: bound every length field before allocating, cap recursion and include-style expansion (circular references become an infinite loop), and wrap the parse in try/catch. A malicious server otherwise hangs the renderer even where memory safety rules out RCE.
  • Consult frontend-security for the renderer's own browser surface — Content Security Policy, DOM sinks, sanitizer choice. The renderer is a browser; that skill owns what runs inside it, this one owns what it can reach beyond it.

NEVER

  • Set nodeIntegration: true, disable contextIsolation, disable sandbox, disable webSecurity, or set allowRunningInsecureContent: true — especially on a window that loads remote or navigable content.
  • Leave RunAsNode or the Node CLI / NODE_OPTIONS fuses enabled in a release build.
  • Encrypt tokens at rest with app-level crypto in place of safeStorage, or keep a PLAINTEXT: fallback path. crypto-misuse owns cipher mode, key derivation and the rest.
  • Ship a frameless or chromeless navigable window (frame: false, no address bar): after a redirect the user has no visual cue they left the app, so a phished navigation can silently clone your UI. Frameless is acceptable only behind a hard navigation guard.
  • Assume the renderer, or the content it renders, is the only untrusted input. A backend or update server the app trusts can itself be compromised, or in a multi-tenant deployment driven by another tenant.

KNOWN FALSE POSITIVES

  • Dev builds that load http://localhost:<port> with relaxed settings — these rules govern release builds; the finding is dev config that ships.
  • A custom protocol (myapp://) for OAuth callbacks is expected. The control is state / PKCE validation, not the scheme's existence.
  • contextBridge-exposed functions are intentional capabilities. Review what each one does and whether it validates input, not the fact that a bridge exists.
  • shell.openExternal on a hard-coded https:// constant, not user input.
  • A frameless window loading only local first-party content behind a deny-by-default nav guard. The risk is frameless plus navigable to remote origins.
  • Connecting to a backend and rendering its data — telemetry JSON, numbers, binary frames — is normal desktop behaviour. The control is bounding and validating that data, and never treating it as HTML, a filesystem path, or a shell argument. Not avoiding the connection.

Context (for humans)

Electron ships a Chromium renderer and a Node main process in one app. The classic misconfiguration is nodeIntegration: true with no navigation guards: it turns any renderer-side bug — XSS in a report view, an open redirect, a deep link — into full host RCE, because every IPC handler the preload exposes is then reachable by attacker-controlled renderer code.

Two things have shifted since that framing was written. The dangerous window options are now safe by default, so the modern finding is code that re-enables them rather than code that forgets to set them. And the runtime boundary is no longer the whole story: the packaging fuses decide whether the shipped binary can be re-launched as a plain Node process at all, which is a hole no webPreferences setting can close.

The renderer is the first untrusted boundary but not the only one. A desktop app also trusts every server it connects to. Where that server is shared — a multi-tenant backend, a per-session remote node — a co-tenant who compromises it becomes an attacker feeding your privileged renderer. The worst chain is the reverse of the obvious one: server-controlled content → a navigable preload-bearing window → the exposed API → local RCE.

References

Signals

GitHub stars
22
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
electron-security
Source
github.com/shieldnet-360/secure-vibe