Back to Home

Electron Guide - Cross-Platform Desktop Apps

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.

TRUSTED ZONEMain ProcessFile System AccessNative APIs & ShellCredential StorageIPCSECURITY BOUNDARYUNTRUSTED ZONERenderer ProcessUser-Generated ContentThird-Party LibrariesRemote Content

🎯 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."

1

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.

2

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.

3

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.

contextIsolation: true in all windows
nodeIntegration: false in all windows
sandbox: true for renderers
CSP implemented and tested
All IPC inputs validated
File paths sanitized (no traversal)
Credentials stored with safeStorage
Electron updated to latest stable

Security Mindset

Assume renderer is compromised—validate everything
Minimize preload API surface area
Keep Electron and dependencies updated
Run npm audit regularly
Use HTTPS for all external resources
Never load remote content with privileges
Encrypt sensitive data at rest
Test with security-focused tools (Snyk, etc.)

Security 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.