Securing Electron Apps with Context Isolation
Learn how Electron’s context isolation feature prevents renderer code from accessing Node.js APIs directly and how to expose only the functions you need via contextBridge.
25 Sept 2025, 13:17 UTC

The problem: accidental Node exposure in Electron
Many Electron applications start with the default webPreferences settings, which historically allowed renderer code to call require or use the remote module. When the renderer loads untrusted content (for example, a remote web page or user‑generated HTML), those APIs give the page direct access to the underlying Node.js environment. This can lead to remote code execution, file system access, or other serious security issues.
Thesis: enable context isolation and expose only what you need
Electron’s context isolation feature creates a separate V8 JavaScript context for the preload script. The renderer runs in its own context and cannot reach Node.js globals or the preload script’s scope directly. By pairing this with contextBridge, you can deliberately expose a limited set of functions (or values) to the renderer while keeping the rest of Node.js hidden.
How to turn on context isolation
In the main process where you create BrowserWindow instances, set contextIsolation to true. This is the default for Electron 12 and later, but being explicit avoids surprises if you ever downgrade.
// main.js
const { app, BrowserWindow } = require('electron');
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Enable the isolated context for the preload script
contextIsolation: true,
// Point to your preload file
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
No special permissions are required to edit the source; you just need read/write access to your project folder. After changing the file, restart the Electron process to apply the new webPreferences.
Exposing APIs safely with contextBridge
The preload script runs in the isolated context, but you can still send selected functions to the renderer using contextBridge.exposeInMainWorld. Only the properties you pass become available on window (or a custom namespace).
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Expose a simple messenger API
contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (channel, data) => {
// Validate channel name if desired
ipcRenderer.send(channel, data);
},
// Example of a synchronous‑looking helper that still uses IPC
getAppVersion: () => ipcRenderer.sendSync('get-version')
});
In the renderer (index.html or any loaded page) you can now call:
// renderer.js (executed in the page)
async function greet () {
await window.electronAPI.sendMessage('greet', 'hello from renderer');
}
// You can also read a value exposed via sync IPC
const version = window.electronAPI.getAppVersion();
console.log('App version:', version);
Because the preload script is isolated, the renderer cannot reach require, process, or any other Node globals unless you explicitly expose them.
Verifying that isolation is active
Open the developer tools for the renderer window and run:
> window.contextIsolation
// Expected output: true
Then check that only the intended API is present:
> window.electronAPI
// Should show an object with sendMessage and getAppVersion, nothing else.
If you see false for window.contextIsolation or unexpected properties like require, the isolation is not enabled or the preload script is leaking globals.
Trade‑offs and limitations
- Refactoring effort: Existing code that relied on
requirein the renderer or theremotemodule must be moved to the preload script or replaced with IPC calls. - Performance: Crossing the context boundary adds a small overhead (typically sub‑millisecond per call). For most desktop apps this is negligible, but high‑frequency loops should be avoided.
- Serializable values: Objects passed through
contextBridge must be either primitive types or plain objects that can be serialized; complex class instances or functions with closures may not behave as expected.
Actionable closing
- Set
contextIsolation: true(or rely on the default) in everyBrowserWindowyou create. - Move any Node.js access you need to expose into a preload script.
- Use
contextBridge.exposeInMainWorldto whitelist exactly the functions or values the renderer should see. - Verify with
window.contextIsolation === trueand inspect the exposed API in the renderer console. - Run your app with untrusted content (e.g., a remote URL) and confirm that attempts to call
requirethrow an error.
By following these steps you keep the renderer sandboxed, reduce the attack surface, and still provide the limited Node.js capabilities your application needs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.