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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add ShieldNet-360/secure-vibe --skill electron-security --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Electron Security?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-electron-security)More formats (shields.io, HTML) on the badges page.
---
name: electron-security
description: "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."
---
<!-- Native skill bundle for agent-skills (cross-tool convention). Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/electron-security/SKILL.md. -->
# Electron Desktop Security
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.
## 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.
## Reference files
Read these only when the task calls for them.
- `references/electron-hardening.md`
- `references/verifying-findings.md`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!