Securing Electron Renderers with Context Isolation and contextBridge
Learn how Electron’s context isolation blocks XSS‑driven Node access and how to safely expose only the APIs you need via contextBridge.
21 Jan 2026, 14:32 UTC

The problem: XSS can give attackers full Node access
When an Electron app loads remote content or accepts user‑generated HTML, a cross‑site scripting (XSS) flaw can let malicious JavaScript run inside the renderer process. If the renderer shares the same JavaScript context as Electron’s Node integration (nodeIntegration: true), that script can call require, process, or any Electron module and gain unrestricted access to the host operating system.
Thesis: Enable context isolation and expose only what you need via contextBridge
Electron’s built‑in context isolation feature (enabled by default since Electron 12) runs the renderer in a separate JavaScript context. In that isolated context, globals like require and process are undefined, forcing any privileged interaction to go through a preload script that explicitly whitelists safe functions using contextBridge.exposeInMainWorld. This creates a clear security boundary while still allowing the UI to call necessary Electron APIs.
How context isolation works
When webPreferences.contextIsolation: true is set, Electron creates a distinct V8 context for the page. The preload script runs in a context that has access to Node and Electron modules, but the rendered page does not. Communication happens only through the objects you expose on window via the bridge.
Minimal preload script
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Expose a safe API to the renderer
contextBridge.exposeInMainWorld('electronAPI', {
// Example: read a file from the user's documents folder
readFile: (fileName) => ipcRenderer.invoke('read-file', fileName),
// Example: send a message to the main process
sendMessage: (channel, ...args) => ipcRenderer.send(channel, ...args)
});
The renderer can now call window.electronAPI.readFile('notes.txt') without ever seeing require or process.
Worked example: building a tiny Electron app
- Create a project folder and initialize
npm init -y. - Install Electron:
npm install --save-dev electron. - Add the following files:
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs').promises;
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // explicit, but true is default since v12
nodeIntegration: false // keep Node integration off in the renderer
}
});
win.loadFile('index.html');
}
ipcMain.handle('read-file', async (event, fileName) => {
const userData = app.getPath('documents');
const filePath = path.join(userData, fileName);
return fs.readFile(filePath, 'utf8');
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
// index.html
Context Isolation Demo
File Reader
Read
Open DevTools in the renderer window and run:
typeof require→ "undefined"typeof process→ "undefined"window.electronAPI.readFile→ a function
This confirms that the renderer is isolated yet can still invoke the exposed API.
Trade‑off: boilerplate and API review
Enabling context isolation forces you to move any direct Node or Electron calls out of the renderer and into the preload script. For existing codebases this can mean adding a thin wrapper layer, which adds a few lines of boilerplate. More importantly, the security guarantee depends on what you expose via contextBridge. Accidentally exposing powerful primitives like require, child_process.exec, or the full ipcRenderer object can re‑introduce the same attack surface. Teams should audit the exposed functions and keep the API surface as small as practical.
Limitation: debugging and devtools
Because the renderer cannot access require, some debugging conveniences (e.g., importing a local module directly in the console) are unavailable. If you need to test a helper function in DevTools, you must first expose it through the bridge or temporarily disable isolation in a debug build—never in production.
Actionable checklist
- Set
webPreferences: { contextIsolation: true, nodeIntegration: false }for everyBrowserWindow. - Create a preload script that uses
contextBridge.exposeInMainWorldto whitelist only the functions your UI truly needs. - Verify in DevTools that
requireandprocessare undefined while your exposed API works. - Regularly review the exposed API surface; avoid passing through raw Electron modules.
- Keep Electron updated; context isolation defaults to true, but explicit configuration protects against future defaults changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.