Securing Electron Renderers with Context Isolation and Preload APIs
Prevent renderer code in Electron from accessing Node APIs by enabling contextIsolation, disabling nodeIntegration, and exposing a safe API via a preload script. This guide shows the minimal config, trust boundaries, checks, and failure modes.
13 Oct 2025, 13:30 UTC

Problem & Takeaway
When an Electron renderer can call require('fs') or any Node API, a malicious script injected into the page can read the file system, execute arbitrary code, or exfiltrate data. The simplest, most effective way to prevent this is to enable contextIsolation and disable nodeIntegration, then expose a tightly‑controlled API surface via a preload script. This design keeps the renderer in a pure web context while still allowing the app to perform necessary I/O through the main process.
Requirements
- Electron 12 or newer (contextIsolation defaults to true).
- Node.js v14+ for the main process.
- Preload scripts served from a trusted local path.
- Content Security Policy (CSP) if loading remote content.
Minimal Design
Below is the smallest configuration that satisfies the requirements:
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
contextIsolation: true, // Isolate renderer context
nodeIntegration: false, // Remove Node globals
preload: path.join(__dirname, 'preload.js') // Preload script
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
The preload script runs in the renderer’s isolated context but has access to the electron module. It uses contextBridge to expose only the APIs the renderer needs.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
log: (msg) => console.log(msg)
});
In the main process, the IPC handler performs the actual file read:
const { ipcMain } = require('electron');
const fs = require('fs').promises;
ipcMain.handle('read-file', async (event, filePath) => {
try {
return await fs.readFile(filePath, 'utf8');
} catch (err) {
return { error: err.message };
}
});
Now the renderer can call window.api.readFile('/etc/passwd') without ever accessing require or process directly.
Trust & Data Boundaries
- Main Process owns all privileged operations (file system, OS APIs, native modules).
- Preload Script mediates between the renderer and main process, exposing a curated API surface.
- Renderer operates in a sandboxed web context; it cannot import Node modules or access
process.
Operational Checks
- Verify Electron version in the main process:
console.log(process.versions.electron); - Confirm context isolation in the renderer console:
console.log('require' in window); // should be false - Inspect exposed API via DevTools
Applicationtab; theWindowobject should contain onlyapi(or your chosen surface). - Test IPC call from renderer:
window.api.readFile('test.txt').then(console.log);
Any deviation (e.g., window.require defined) indicates a misconfiguration that could expose Node APIs.
Failure Modes
- contextIsolation disabled: Renderer gains full Node access; injection can read entire file system.
- nodeIntegration enabled: Even with contextIsolation, Node globals are available in the renderer.
- Preload script fails to load (e.g., path typo): The renderer will have no
apisurface, breaking functionality or falling back to insecure defaults. - Remote content loaded without CSP: XSS can still compromise the isolated context if the CSP is weak.
When the Design Should Change
- If legacy code requires nodeIntegration to function, you must either refactor the renderer to use the preload API or adopt a different IPC pattern (e.g.,
ipcRenderer.invokefor each action). - For complex inter‑process communication that cannot be expressed as simple exposed APIs, consider using
ipcMain.handlefor each channel or a dedicated background worker with its own sandbox. - When the application must load external web content, you must enforce a strict CSP and possibly run the external content in a separate
BrowserViewwith its own isolated context.
Conclusion
Enabling contextIsolation with nodeIntegration disabled and exposing a minimal API via a preload script is the smallest, most robust architectural change to protect Electron renderers. It keeps privileged operations in the main process, prevents accidental Node exposure, and allows the renderer to perform required tasks securely.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.