agentsclimarketplace

Electron security

Skill ShieldNet-360/secure-vibe/skills/electron-security

Harden Electron apps: renderer trust boundary (nodeIntegration, contextIsolation, sandbox), contextBridge/IPC allowlists, shell.openExternal, navigation guards, deep-link auth, safeStorageFrom its SKILL.md

Install
npx -y skills add ShieldNet-360/secure-vibe --skill electron-security

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.8 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it

Electron Desktop Security

Rules (for AI agents)

The unifying principle: 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. Lock the renderer-trust boundary first; every IPC sink is reachable from a compromised renderer.

ALWAYS

  • Configure every BrowserWindow with nodeIntegration: false, contextIsolation: true, and sandbox: true. The preload + contextBridge is the supported way to give the renderer capabilities — the page never needs Node.
  • 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.
  • 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. 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.
  • Allowlist shell.openExternal to https: (and mailto: if needed) after parsing the URL. Reject file:, custom schemes, and anything else.
  • 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.
  • Remember the contextBridge surface is exposed to whatever origin the webContents currently holdsexposeInMainWorld does not re-check origin after a navigation. So a single missing will-navigate guard lets a remote / attacker origin inherit your entire IPC surface (this is how a stored hyperlink → navigation becomes 1-click RCE). The nav guard is the primary control; as defense-in-depth, gate the preload on location before exposing.
  • Before attaching session tokens / cookies to an outbound request, verify the target host is on your own-API allowlist. Never attach credentials to a renderer-supplied URL.
  • 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.
  • Store tokens with Electron safeStorage (OS keychain / DPAPI / libsecret), not app-level crypto. Enable ASAR integrity + code signing for release.
  • Treat every server the app connects to — backend, simulation / compute node, auto-update channel, a multi-tenant cloud session — as potentially attacker-controlled (compromised, co-tenant, or MITM). Never load a server URL into a BrowserWindow / <webview> that carries your preload, and never feed a server response into an IPC sink (file path, shell arg, openExternal URL) without the same validation you apply to renderer input.
  • Harden parsers that consume untrusted server / stream data (binary frames, SDF / XML, model files): bound every length field before allocating, cap recursion and <include>-style expansion (circular refs → infinite loop / fetch), and wrap the parse in try/catch. Otherwise a malicious server crashes or hangs the renderer (DoS), even when memory-safety prevents RCE.

NEVER

  • Set nodeIntegration: true, disable contextIsolation, disable webSecurity, or set allowRunningInsecureContent: true — especially when the window loads remote or navigable content.
  • Concatenate renderer input into a shell string (child_process.exec(\docker kill ${names}`)`) — command injection.
  • Concatenate a renderer-supplied path for fs read/write (${BASE}${filePath}) — path traversal / arbitrary file write.
  • Call shell.openExternal on an arbitrary or renderer-controlled URL — file: / custom protocol handlers are a local-launch / RCE vector.
  • Attach Authorization / session cookies to a URL the renderer chose without a host allowlist — XSS then exfiltrates the token.
  • Accept a deep-link auth token (myapp://auth?refresh-token=…) without origin / state validation — login CSRF / session fixation.
  • Encrypt tokens at rest with AES-CBC (no integrity) or a key derived solely from a locally recoverable machine ID, and never keep a PLAINTEXT: fallback path.
  • Ship a frameless / 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 its rendered content) is the only untrusted input — a backend / sim / update server the app trusts can itself be compromised or, in multi-tenant deployments, driven by another tenant.

KNOWN FALSE POSITIVES

  • Dev builds that load over http://localhost:<port> with relaxed settings — the rules apply to release builds; ensure dev config never 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 the bridge exists.
  • shell.openExternal on a hard-coded https:// constant (not user input) is fine.
  • A frameless window that loads only local first-party content (file:// / packaged app) behind a deny-by-default nav guard is fine — 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 / validating that data and never treating it as HTML, a filesystem path, or a shell argument — not avoiding the connection itself.

Context (for humans)

Electron ships a Chromium renderer and a Node main process in one app. The single most dangerous misconfiguration is nodeIntegration: true with no navigation guards: it turns any renderer-side bug (XSS in a report/telemetry view, an open redirect, a deep link) into full host RCE. Every IPC handler the preload exposes — file write, process exec, openExternal, token access — is then reachable by attacker-controlled renderer code.

AI assistants frequently generate Electron windows with nodeIntegration: true (it makes require "just work" in the page), child_process.exec with template-string interpolation, shell.openExternal(url) with no validation, and custom token crypto instead of safeStorage. This skill is the counterweight: keep the renderer sandboxed and every IPC sink validated.

The renderer is the first untrusted boundary, but not the only one: a desktop app also trusts every server it connects to. In multi-tenant or cloud-compute setups (a shared simulation backend, a per-session remote node), a co-tenant who compromises that server becomes an attacker feeding your privileged renderer — so server URLs must never load into a preload-bearing window, and server data is input to be validated, not trusted. The worst observed chain is the reverse of the obvious one: server-controlled content → a navigable preload window → electronAPI → local RCE.

Verify & lock (triaging a finding)

A scanner/review hit (electronegativity flag, a webPreferences line, a raw exec/openExternal call) is a candidate, not a confirmed bug. Confirm it, fix it, then lock it so it can't come back.

  1. Confirm it's real (probe / inspect). Load a test page in the release build's renderer and try to reach Node: window.require, window.process, require('child_process').exec(...). Real if any resolve — nodeIntegration:true or contextIsolation:false/sandbox:false. For an IPC/sink hit, drive the exposed electronAPI function with hostile input (../../etc/passwd, a; id, file:///…, a foreign host for token attach) and watch whether the main process traverses/spawns/opens. For nav, attempt a will-navigate or window.open to an external origin and see if it lands. FP if isolation + sandbox are on and Node calls throw, the sink type-checks/allowlists and rejects, the nav guard denies by default, or it's dev-only config / a hard-coded https: constant / intentional myapp:// OAuth with state/PKCE.
  2. Fix, then lock with a regression test (unit or integration — dev's call). Assert the locked boundary: renderer cannot reach require/Node; every BrowserWindow carries nodeIntegration:false, contextIsolation:true, sandbox:true, webSecurity:true; IPC handlers reject traversal/injection/ foreign-host args; the nav guard denies an external origin and setWindowOpenHandler returns deny; tokens go through safeStorage with no PLAINTEXT: fallback. Add one benign in-app action (valid IPC call, allowed https: open) that still works. Commit it so the guard can't be silently dropped.

References

What ships with it: 2 files

7.1 KB alongside SKILL.md

checklists/

tests/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.