Securing Electron IPC with Context Isolation and ContextBridge
Learn how to expose only the needed IPC methods to your renderer using Electron's Context Isolation and ContextBridge, preventing accidental Node.js leakage.
10 Nov 2025, 17:46 UTC

The problem: accidental Node.js exposure in Electron apps
When you build an Electron application, the renderer process runs a Chromium‑based web page. If that page can call require or access process directly, a compromised frontend (for example via an XSS vulnerability) gains full Node.js power on the user’s machine. Historically, developers turned on nodeIntegration: true to make this easy, but that disables a core safety net.
The goal is to keep the renderer sandboxed while still allowing it to ask the main process to perform privileged operations (reading files, accessing native modules, etc.). Electron’s answer is a combination of Context Isolation and the ContextBridge API.
How Context Isolation works
With contextIsolation: true (the default since Electron 12), the preload script and the renderer’s JavaScript execute in separate V8 contexts. Even though they share the same HTML document, objects created in one context are not directly accessible from the other. This prevents the renderer from reaching into the preload script’s Node.js globals unless you explicitly expose them.
Verification: open DevTools on a running Electron window and try typeof require. If isolation is working, the result will be "undefined".
Exposing a safe API with ContextBridge
The contextBridge module lets you whitelist specific functions or values and copy them into the renderer’s window object. The exposed API is cloned via the Structured Clone Algorithm, so only serializable data (or transferable objects) can cross the boundary.
A common pattern is the “gateway” preload script:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Define a narrow IPC gateway
contextBridge.exposeInMainWorld('electronAPI', {
// Asynchronous request‑response
readFile: (path) => ipcRenderer.invoke('read-file', path),
// Synchronous notification (fire‑and‑forget)
logMessage: (msg) => ipcRenderer.send('log-message', msg),
});
Only the two methods readFile and logMessage are visible to the page. The rest of ipcRenderer (including on and removeListener) stays hidden, reducing the attack surface.
Main‑process side: handling the calls
In the main process you listen for the invoked channel and return a promise. Using ipcMain.handle ensures that errors are automatically propagated as rejected promises.
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs').promises;
function createWindow() {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // explicit for clarity
},
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
ipcMain.handle('read-file', async (event, path) => {
// Validate path, restrict to allowed directories, etc.
return fs.readFile(path, 'utf8');
});
ipcMain.on('log-message', (event, msg) => {
console.log('[Renderer log]', msg);
});
The renderer can now call:
// index.html (renderer script)
async function loadConfig() {
try {
const content = await window.electronAPI.readFile('config.json');
console.log('File content:', content);
} catch (err) {
console.error('Failed to read file:', err);
}
}
window.electronAPI.logMessage('Config load started');
loadConfig();
Trade‑offs and limitations
- Serialization cost: Complex objects (classes, functions, circular structures) are lost or throw errors because they cannot be cloned. You must pass plain JSON‑compatible data or use Electron’s transferable objects (e.g.,
ArrayBuffer) for binary data. - Over‑exposure risk: If you accidentally expose a powerful Node.js API (like
requireorchild_process) through the bridge, a compromised renderer can still abuse it. Keep the exposed surface as small as possible and validate all inputs on the main‑process side. - Debugging friction: Because the preload script runs in an isolated context, you cannot access its variables directly from the DevTools console unless you expose them via
contextBridge. This is intentional for security but adds a step when troubleshooting.
Practical check: after building, open DevTools, try window.electronAPI (should exist) and require (should be undefined). If either check fails, review your webPreferences.
Actionable closing
Start with the default secure settings (contextIsolation: true, nodeIntegration: false). Use a preload script that exposes only the exact IPC methods your UI needs, preferring ipcRenderer.invoke / ipcMain.handle for request‑response flows. Validate and sanitize every argument in the main process, and keep the bridge free of Node.js globals. This approach gives you the functionality of a desktop app while preserving the sandbox‑like safety of a web page.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.