Securing Electron Apps with Context Isolation: Why, How, and What to Watch Out For
Enable Electron’s context isolation to protect your app from injection attacks. Learn how to configure BrowserWindow, expose safe APIs via preload scripts, verify isolation, and handle trade‑offs with a practical example.
05 Apr 2026, 09:55 UTC

Why Context Isolation Matters
When you build an Electron app, the renderer process (the web page you see) runs in a sandboxed environment that normally has access to Node.js and Electron APIs. This convenience is also a security liability: any script injected into the renderer—whether from a malicious plugin, a compromised third‑party library, or a user‑supplied file—can call require, access the file system, or manipulate the main process. Context Isolation is Electron’s built‑in guardrail that separates the renderer’s JavaScript world from the privileged Electron/Node world.
Thesis: Enable Isolation, Expose Only What You Need
By setting contextIsolation: true in the webPreferences of a BrowserWindow, you force Electron to run the renderer in a clean, isolated context. The renderer cannot reach Node globals. To still provide useful functionality, you must expose a curated API via a preload script that runs in the same context as the renderer but has access to Node and Electron modules. The contextBridge API is the recommended way to do this.
1. Enabling Context Isolation in BrowserWindow
In your main process (e.g., main.js), create the window like this:
const { app, BrowserWindow } = require('electron');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // Path to preload script
contextIsolation: true,
nodeIntegration: false, // must be false when isolation is on
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
Key points:
- Set
contextIsolation: true. - Set
nodeIntegration: false—Electron will ignore Node APIs in the renderer if both are enabled. - Specify a
preloadscript that will run before any renderer code.
2. Exposing a Safe API with contextBridge
In preload.js, you can expose a small API to the renderer. For example, let’s expose a function that returns the Electron app version:
const { contextBridge, ipcRenderer } = require('electron');
// Expose a read‑only API to the renderer
contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('get-app-version')
});
Notice:
- The API is exposed under
window.electronAPIin the renderer. - It calls
ipcRenderer.invoketo ask the main process for the data, keeping the renderer free of Node modules.
3. Implementing the Main‑Side Handler
In the main process, add a handler for the IPC channel:
const { ipcMain, app } = require('electron');
ipcMain.handle('get-app-version', async () => {
return app.getVersion();
});
4. Using the Exposed API in the Renderer
In your renderer (e.g., index.html), call the API like this:
document.getElementById('btn').addEventListener('click', async () => {
const version = await window.electronAPI.getAppVersion();
console.log('App version:', version);
});
If context isolation is working, the following will throw a ReferenceError in the console:
console.log(process); // ReferenceError: process is not defined
console.log(require('electron')); // ReferenceError: require is not defined
5. Trade‑offs and Debugging Challenges
- Debugging isolation: When context isolation is on, console logs from the renderer are still visible, but stack traces may omit frames from the preload script. Tools like
node-inspectorcannot attach to the renderer process because it no longer has a Node context. - Performance: The isolation boundary adds a tiny overhead when the renderer calls into the preload or IPC, but it’s negligible for most apps.
- Third‑party libraries: If you load a library that expects
processorrequireto be present, it will fail unless you polyfill those globals in the preload. - Global state sharing: Do not attempt to share state via global variables exposed by
nodeIntegration. UseipcRenderer/ipcMainchannels instead.
6. Best Practices for a Secure, Maintainable Setup
- Keep preload scripts minimal—only expose the APIs you actually need.
- Never enable
nodeIntegrationwhencontextIsolationis on. - Bundle or transpile preload scripts if you use ES6 modules; they run in a separate context.
- Use
contextBridge.exposeInMainWorldto expose read‑only objects; avoid exposing writable references that could be mutated by the renderer. - When you need to share state between windows, route changes through IPC instead of relying on global variables.
Actionable Checklist
- Set
contextIsolation: trueandnodeIntegration: falseinwebPreferences. - Create a lightweight
preload.jsthat usescontextBridgeto expose safe APIs. - Implement IPC handlers in the main process for those APIs.
- In the renderer, access the APIs via
window.electronAPI. - Verify isolation:
- Open DevTools, type
processorrequire—they should be undefined. - Try calling an exposed API and confirm it returns the expected value.
- Check the global object in DevTools; only the exposed properties should appear.
- Open DevTools, type
- Test with a third‑party library that expects Node globals; if it fails, consider polyfilling or refactoring.
- Document the exposed API surface in your project README so other developers know what is available.
Conclusion
Context isolation is a powerful feature that turns a convenience (Node APIs in the renderer) into a potential vulnerability. By enabling it and carefully exposing only the APIs you need through contextBridge, you dramatically reduce the attack surface while keeping your app functional. Just remember the trade‑offs—debugging gets a bit trickier, and you must avoid relying on global Node objects in the renderer. Follow the checklist above to get a secure, maintainable Electron application in no time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.