Install
$ agentstack add skill-hlsitechio-claude-skills-security-electron-security ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
About
Electron Security Audit
Audit Electron desktop apps. Electron combines a Chromium renderer with a Node main process — the most dangerous configurations let renderer code call Node APIs directly.
When this skill applies
- Reviewing BrowserWindow webPreferences
- Auditing preload scripts and contextBridge usage
- Reviewing IPC channels for unsafe exposure
- Checking deep link / protocol handler implementations
- Reviewing auto-updater configuration
Workflow
Follow ../_shared/audit-workflow.md.
Phase 1: Stack detection
grep -E '"electron":' package.json
electron --version 2>/dev/null
find . -name 'electron-builder.*' -o -name 'forge.config.*' 2>/dev/null
Electron version matters — Chromium and Node versions update together; old Electron = old Chromium = unpatched browser CVEs.
Phase 2: Inventory
# BrowserWindow configs
grep -rn 'new BrowserWindow\|webPreferences' src/ main/ 2>/dev/null
# Preload references
grep -rn 'preload:' src/ main/ 2>/dev/null
# IPC handlers
grep -rn 'ipcMain.handle\|ipcMain.on\|ipcRenderer.send\|ipcRenderer.invoke' src/ main/ renderer/ 2>/dev/null
# Context bridge
grep -rn 'contextBridge.exposeInMainWorld' src/ main/ 2>/dev/null
# Webview tags
grep -rn '/dev/null
# Protocol handlers
grep -rn 'app.setAsDefaultProtocolClient\|protocol.registerSchemesAsPrivileged' src/ main/ 2>/dev/null
Phase 3: Detection — the checks
BrowserWindow webPreferences
The default-secure pattern (Electron 12+):
new BrowserWindow({
webPreferences: {
contextIsolation: true, // ← must be true
nodeIntegration: false, // ← must be false
sandbox: true, // ← preferred true
webSecurity: true, // ← default true; never disable
allowRunningInsecureContent: false, // ← never true
preload: path.join(__dirname, 'preload.js'),
},
});
- ELC-WP-1
contextIsolation: true. False = renderer can access Node APIs directly via globals. - ELC-WP-2
nodeIntegration: false. True =require('child_process').exec(...)works from any rendered page. - ELC-WP-3
sandbox: truewhen feasible. Sandboxed renderer can't use Node even in preload (use contextBridge for safe IPC). - ELC-WP-4
webSecurity: true(default). Never disable; turns off same-origin policy. - ELC-WP-5
allowRunningInsecureContent: false. - ELC-WP-6
experimentalFeatures: false. - ELC-WP-7
enableRemoteModule: false(removed in Electron 14+; check for@electron/remoteuse which has similar risks).
The above is non-negotiable for any app loading remote content. For apps that load only local files, defaults are still recommended (defense in depth).
Preload scripts and contextBridge
With contextIsolation: true, the preload script has access to a privileged context separated from the renderer's web context. To expose APIs to the renderer:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
// EXPOSE specific, narrow APIs
saveFile: (data) => ipcRenderer.invoke('save-file', data),
loadFile: (path) => ipcRenderer.invoke('load-file', path),
});
- ELC-PL-1 Preload doesn't expose generic functions like
eval,require, fullipcRenderer, orfsdirectly. - ELC-PL-2 Each exposed function in
api.*corresponds to a vetted main-process handler. The renderer can call only what's exposed. - ELC-PL-3 Exposed functions don't accept renderer-controlled arguments that the main process trusts as filesystem paths, command strings, or URLs without validation.
IPC handlers
// main.js
ipcMain.handle('load-file', async (event, filepath) => {
// BAD — renderer can pass any path
return fs.readFile(filepath);
// GOOD — validate path is in allowed directory
const safeRoot = path.resolve(app.getPath('userData'), 'documents');
const resolved = path.resolve(safeRoot, filepath);
if (!resolved.startsWith(safeRoot + path.sep)) {
throw new Error('Forbidden path');
}
return fs.readFile(resolved);
});
- ELC-IPC-1 Every
ipcMain.handle/ipcMain.onvalidates inputs from the renderer. - ELC-IPC-2 No IPC handler does shell command execution with renderer-supplied args.
- ELC-IPC-3 No IPC handler reads arbitrary filesystem paths — confine to known directories.
- ELC-IPC-4 Verify
event.senderFrameif multiple webContents could send IPC; subframes are a vector. - ELC-IPC-5 No
ipcMain.on(fire-and-forget) for security-sensitive operations — useipcMain.handleso renderer can't replay without ack.
Webview tag
`` is the in-app browser; it's notoriously dangerous.
- ELC-WV-1 `
disabled if not used — setwebviewTag: false` in webPreferences. - ELC-WV-2 If used: handle
will-attach-webviewto constrain webPreferences:
``js app.on('web-contents-created', (event, contents) => { contents.on('will-attach-webview', (e, webPreferences, params) => { delete webPreferences.preload; webPreferences.nodeIntegration = false; webPreferences.contextIsolation = true; // Block navigation to internal pages if (!params.src.startsWith('https://')) e.preventDefault(); }); }); ``
- ELC-WV-3 Prefer `
with appropriatesandboxover` when possible.
Navigation control
Renderer should not be able to navigate the BrowserWindow to arbitrary URLs:
- ELC-NAV-1
will-navigatehandler restricts navigation to allowlisted origins:
``js contents.on('will-navigate', (e, url) => { const parsedUrl = new URL(url); if (!['app.yourorg.com'].includes(parsedUrl.host)) { e.preventDefault(); } }); ``
- ELC-NAV-2
setWindowOpenHandler(replacesnew-window) restricts whatwindow.open/ target=_blank does:
``js contents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); // open in default browser return { action: 'deny' }; }); ``
Content Security Policy
- ELC-CSP-1 App's CSP set via meta tag or main-process header injection. Same patterns as web — restrict script-src, no
unsafe-inlineunless required. - ELC-CSP-2 Local content uses
app://orfile://carefully; consider registering a custom protocol and serving from it (better security model than file://).
Deep links / protocol handlers
app.setAsDefaultProtocolClient('myapp');
app.on('open-url', (event, url) => {
// url is from any source — validate
});
- ELC-DL-1 Protocol handler validates the URL structure before acting.
- ELC-DL-2 Deep links don't trust embedded data (e.g.,
myapp://auth?token=...— verify the token, don't blindly accept). - ELC-DL-3 On macOS, the
open-urlevent fires when the app is opened via the protocol; queue handling until app is ready.
Auto-updater
- ELC-AU-1 Updates served over HTTPS only.
- ELC-AU-2 Updates signed; signature verified before installation (electron-updater handles this if configured).
- ELC-AU-3 Update server URL verified — not user-configurable.
- ELC-AU-4 Squirrel.Windows / NSIS updaters have known issues with installer hijacking on Windows; confirm using current electron-updater versions with signed installers.
Native dependencies
- ELC-NAT-1 Native modules (
node-gyp-compiled) audited and pinned. Native modules execute with full process privileges. - ELC-NAT-2 ASAR archive integrity considered — ASAR files are not encrypted; anyone with the binary can extract source. Don't put secrets in ASAR.
Electron version
- ELC-VER-1 Electron version current. Chromium CVEs land in Electron releases roughly monthly; old Electron = exploitable browser. Check
electron --versionmatches a current supported line (last 2-3 majors).
Process model
- ELC-PM-1 Main process doesn't run user-supplied JavaScript via
vmorFunction. - ELC-PM-2 Renderer-to-renderer IPC (between BrowserWindows) goes through main; no shared globals.
Phase 4: Triage
Critical: nodeIntegration: true; missing context isolation; IPC handler executing shell with renderer-controlled args; webview with permissive config.
Phase 5: Report
Use ../_shared/findings-schema.md. Prefix IDs with ELC-.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: hlsitechio
- Source: hlsitechio/claude-skills-security
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.