Security Best Practices
Electron apps combine web content with Node.js power. That's dangerous if you're not careful—but modern Electron has excellent security defaults. Your job is to not disable them.
The core insight: Your renderer process might display untrusted content—user input, third-party libraries, remote websites. If that content can access Node.js, attackers can read files, install malware, or steal credentials. Security is about keeping untrusted code in a sandbox.
Understanding the Threat Model
Think of your Electron app as having two zones: a trusted zone (main process) with full system access, and an untrusted zone (renderer) that might be compromised. Security is about controlling the boundary between them.
🎯 VibeBlaster Security Story
Early VibeBlaster had nodeIntegration: true because it made OAuth flows easier. During a security review, I realized a malicious browser extension could inject JavaScript and access OAuth tokens stored in the renderer.
Switching to full sandboxing took two weeks of refactoring—but now even a compromised renderer can't steal credentials. The tokens live in the main process, encrypted with safeStorage.
The Three Security Pillars
Modern Electron (v20+) has these enabled by default. Your job is to understand why they exist and resist the temptation to disable them for "convenience."
Context Isolation
Your preload script runs in a separate JavaScript context from the renderer. Even if malicious code runs in your renderer, it can't access or modify your preload functions.
Analogy: It's like having two browser tabs that can't see each other's variables. The preload exposes specific APIs via contextBridge—nothing more.
Node Integration Disabled
The renderer can't use require() or access Node.js APIs. It's just a web page—no file system, no child processes, no network sockets.
The threat: Without this, an XSS attack could runrequire('child_process').exec('rm -rf /'). That's game over.
Sandbox Mode
The renderer runs in Chromium's sandbox—the same isolation used in Chrome. Even if an attacker finds a Chromium zero-day, they're still trapped in the sandbox.
Trade-off: Sandbox mode means your preload script can't use Node.js either. All privileged operations must go through IPC to the main process. More work, but much safer.
The Secure Configuration (Copy This)
new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // ✅ Isolate preload from renderer
nodeIntegration: false, // ✅ No Node.js in renderer
sandbox: true, // ✅ Chromium sandbox
webSecurity: true, // ✅ Same-origin policy
}
});This is the default in Electron 20+. If you find yourself changing these, ask "why?" very carefully.
Secure IPC Patterns
IPC is your security boundary. Treat every message from the renderer like an HTTP request from an untrusted client—validate inputs, check permissions, sanitize data.
❌ Dangerous Pattern
// NEVER expose raw IPC!
contextBridge.exposeInMainWorld(
'electron', { ipcRenderer }
);
// Renderer can now call ANY channel
window.electron.ipcRenderer
.invoke('delete-all-files');Exposes entire IPC system. Malicious code can call any handler.
✅ Secure Pattern
// Expose ONLY specific, validated APIs
contextBridge.exposeInMainWorld('api', {
saveFile: (name, content) => {
// Validate before sending
if (typeof name !== 'string')
throw new Error('Invalid');
return ipcRenderer.invoke(
'file:save', name, content
);
}
});Exposes specific functions with validation. Attack surface minimized.
Validate in Main Process
Even with preload validation, validate again in main process handlers. Defense in depth—assume any layer could be bypassed.
ipcMain.handle('file:save', async (event, filename, content) => {
// 1. Type validation
if (typeof filename !== 'string' || typeof content !== 'string') {
throw new Error('Invalid arguments');
}
// 2. Path traversal prevention
const safeName = path.basename(filename); // Strip directory components
const safePath = path.join(app.getPath('userData'), safeName);
// 3. Verify path is within allowed directory
if (!safePath.startsWith(app.getPath('userData'))) {
throw new Error('Path escape attempt blocked');
}
// 4. Size limits
if (content.length > 10 * 1024 * 1024) { // 10MB max
throw new Error('File too large');
}
await fs.writeFile(safePath, content);
return { success: true };
});⚠️ Never Execute User Input
Never pass user input to exec(),eval(), or shell commands. If you must run system commands, use execFile() with a whitelist of allowed commands and array-based arguments (no shell interpolation).
Content Security Policy (CSP)
CSP tells the browser what content is allowed to load and execute. It's your last line of defense against XSS—even if malicious code gets injected, CSP can prevent it from running.
Recommended CSP
<!-- In your index.html -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.yourapp.com;
font-src 'self';
object-src 'none';
base-uri 'self';
">What this blocks:
- • Inline scripts (XSS payloads)
- • External script loading
- • Unauthorized API calls
- • Plugin/embed content
What this allows:
- • Your bundled scripts
- • Inline styles (for React)
- • Images from your domain + HTTPS
- • API calls to your backend
💡 CSP Debugging Tip
CSP violations appear in DevTools console. Start strict, then loosen only what's necessary. Use Content-Security-Policy-Report-Only header during development to see what would be blocked without actually blocking it.
Security Checklist
Before shipping, verify each of these. A single miss can compromise your users.
Security Mindset
npm audit regularlySecurity is a Foundation, Not a Feature
You now understand Electron's security model: trusted main process, untrusted renderer, IPC as the boundary. These patterns protect your users from the moment they install your app.
Next up: Auto-updates. Because the most secure version of your app is always the latest one—and you need a way to get updates to users seamlessly.