Electron App Security: Context Isolation, nodeIntegration, and the RCE Class That Keeps Coming Back

Electron applications run Node.js in a browser context — a combination that creates a distinctive RCE vulnerability class when misconfigured. This guide covers the core security model, the common misconfigurations that lead to remote code execution, and the controls that actually fix them.

Electron combines Chromium’s rendering engine with Node.js. This is what makes apps like VS Code, Slack, Discord, and Obsidian possible — a single codebase targeting every platform with web technology and native OS access. It is also why a cross-site scripting vulnerability in an Electron app is a different threat from XSS in a browser.

In a browser, XSS gives an attacker JavaScript execution in the renderer process, scoped to the browser’s security model. In a misconfigured Electron app, the same XSS can execute arbitrary Node.js code with the privileges of the operating system user running the app. That is the RCE class this guide is about.

The Electron Process Model

Understanding Electron security requires understanding its two-process architecture:

Main process: The Node.js process. Runs with full OS access — filesystem, child processes, native modules, IPC listeners. Only one per app.

Renderer process: The Chromium rendering context that displays the UI. One or more per app. By default, this is similar to a web page.

The boundary between these two processes is the security perimeter. When it is enforced correctly, a compromised renderer cannot reach the main process’s Node.js capabilities. When it is not, XSS becomes RCE.

The Three Misconfigurations That Create RCE

1. nodeIntegration: true

The most direct path. When nodeIntegration is enabled in a renderer window’s webPreferences, the renderer has access to Node.js APIs directly:

// In the renderer process — with nodeIntegration: true
const { exec } = require('child_process');
exec('calc.exe'); // This works.

An XSS payload in a renderer with nodeIntegration: true executes as Node.js. Full OS access, no sandbox, game over.

nodeIntegration defaults to false in Electron 5+, but many apps written before that default change, or apps that explicitly enable it for convenience, remain vulnerable.

Fix: Never set nodeIntegration: true. Use the preload script pattern (see below).

2. contextIsolation: false

Context isolation creates a hard boundary between the renderer’s JavaScript context and the preload script’s context. With contextIsolation: true (the current default), the preload script and the renderer page cannot access each other’s JavaScript variables or functions.

With contextIsolation: false, a malicious renderer can overwrite objects in the preload context — including any functions or APIs the preload exposes. This allows a compromised renderer to escalate through the preload into main-process capabilities.

// Preload script (contextIsolation: false)
window.electronAPI = {
  readFile: (path) => ipcRenderer.invoke('read-file', path)
};

// Renderer XSS can now call window.electronAPI.readFile('/etc/passwd')
// and manipulate what gets passed to the main process

Fix: Always set contextIsolation: true. It is the default in Electron 12+, but apps that explicitly set it to false or that were built before the default change are at risk.

3. Unsafe webContents.executeJavaScript and eval

Even with nodeIntegration: false and contextIsolation: true, apps that pass user-controlled or untrusted content to webContents.executeJavaScript() or eval() in the main process create a RCE path. This is rare but does appear in apps that implement scripting features or macro execution.

The Preload Script Pattern

The secure way to give a renderer access to main-process capabilities is the preload script with contextBridge:

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // Expose only specific operations, not arbitrary IPC
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  readConfig: () => ipcRenderer.invoke('config:read'),
});
// main.js — BrowserWindow creation
const win = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,   // must be true
    nodeIntegration: false,   // must be false
    sandbox: true,            // additional isolation
  }
});

The renderer can call window.electronAPI.openFile() but has no access to Node.js or the broader IPC channel. The preload acts as a least-privilege API surface.

IPC Validation in the Main Process

Even with correct renderer isolation, the main process must validate all IPC messages. An XSS payload can send arbitrary IPC messages through the exposed preload API. If the main process does not validate those messages, an attacker can craft inputs that trigger unintended behaviour.

// main.js — IPC handler with validation
ipcMain.handle('config:read', async (event, key) => {
  // Validate that key is a string and matches expected pattern
  if (typeof key !== 'string' || !/^[a-z_]+$/.test(key)) {
    throw new Error('Invalid key');
  }
  return config.get(key);
});

Never pass user-controlled paths or values to filesystem operations, child process spawning, or eval in IPC handlers without validation.

Content Security Policy

A Content Security Policy in Electron serves the same role as in web applications — limiting what the renderer can load and execute. For Electron, the CSP is set via the session’s webRequest or via meta http-equiv:

// Strict CSP for Electron renderer
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ["default-src 'self'; script-src 'self'"]
    }
  });
});

A strict CSP that prohibits inline scripts and external script sources significantly raises the bar for XSS exploitation by blocking many injection techniques before they can execute.

Auditing an Electron App

When reviewing an Electron app’s security posture, check:

  1. grep -r "nodeIntegration" . --include="*.js" — any nodeIntegration: true in webPreferences
  2. grep -r "contextIsolation" . --include="*.js" — any contextIsolation: false
  3. grep -r "webSecurity" . --include="*.js" — any webSecurity: false (disables same-origin policy)
  4. grep -r "allowRunningInsecureContent" . --include="*.js" — if true, allows mixed content
  5. Review all IPC handlers in main.js for missing input validation
  6. Check CSP configuration for inline script permissions

For third-party Electron apps, check the package.json for the Electron version. Older versions may have different security defaults. Apps on Electron < 5.0 require careful review of nodeIntegration defaults.

CVE Pattern

The Electron XSS-to-RCE pattern has produced confirmed CVEs in VS Code (before architectural improvements), Mattermost Desktop, Wire Desktop, and several other widely-deployed Electron applications. The pattern is reliable enough that security researchers routinely include Electron apps in bug bounty scope and consistently find exploitable instances in apps written before 2020.

If your team ships an Electron application, treat the renderer as an untrusted context regardless of the content it displays. Apply contextIsolation: true, nodeIntegration: false, sandbox mode, and a restrictive CSP as the baseline configuration, not as optional hardening.