Securing Electron Apps with Context Isolation: When and How to Enable It
Learn how Electron’s Context Isolation separates renderer and preload contexts, why enabling it improves security, and how to expose only the APIs you need via contextBridge.
19 Apr 2026, 10:21 UTC

Problem: Untrusted Renderer Code Can Reach Node
When an Electron app loads remote or third‑party HTML, any script running in the renderer process can, by default, call require and access the full Node API. An XSS vulnerability then gives an attacker direct access to the filesystem, child processes, or other privileged capabilities.
Thesis: Enable Context Isolation and expose only what you need
Turning on webPreferences.contextIsolation puts the renderer in a separate JavaScript context from the preload script. The renderer can no longer reach Node globals unless you deliberately expose them through contextBridge.exposeInMainWorld. This limits the attack surface to the explicit API you provide.
How Context Isolation Works
With isolation enabled, Electron creates two distinct V8 contexts:
- Renderer context – executes the page’s own scripts.
- Preload context – runs before the page loads and has access to both Node and the renderer via the bridge.
The preload script can attach functions or objects to window (or a custom name) using the bridge. Anything not exposed stays invisible to the renderer.
Worked Example
- Create
main.jsthat loads a window with isolation on:
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
- Create
preload.jsthat exposes a safe hello function:
const { contextBridge, exposeInMainWorld } = require('electron');
contextBridge.exposeInMainWorld('api', {
hello: () => 'world'
});
- Create a simple
index.htmlthat tries to use Node and the exposed API:
<!DOCTYPE html>
<html>
<body>
<script>
try {
console.log(require); // should be undefined
} catch (e) {
console.log('require is', typeof require);
}
// Use the exposed API
if (window.api && typeof window.api.hello === 'function') {
console.log('api.hello() =>', window.api.hello());
} else {
console.log('api not available');
}
</script>
</body>
</html>
When you run electron . and open DevTools, you should see:
require is undefined(or a ReferenceError if you try to call it).api.hello() => "world"printed to the console.
Repeating the same test with contextIsolation: false will make require a function, demonstrating the loss of isolation.
Trade‑offs and Limitations
- Legacy code breakage – Any renderer script that expects global Node objects (
require,process, etc.) will fail and must be refactored to use the preload bridge. - Exposure surface – Even with isolation, you must audit what you expose via
contextBridge. Over‑exposing powerful Node APIs re‑introduces risk. - Performance – The extra context adds a minimal overhead; for most UI‑only apps it is negligible.
Actionable Closing
If you are starting a new Electron project or updating an existing one:
- Set
webPreferences.contextIsolation: truein everyBrowserWindowyou create. - Provide a preload script that uses
contextBridge.exposeInMainWorldto expose only the specific functions your UI needs. - Test the renderer with
requireattempts to confirm they are undefined, and verify that your exposed API works. - If you encounter breaking changes, refactor the affected renderer code to call the exposed API instead of accessing Node directly.
By keeping the renderer sandboxed and limiting the bridge to a narrow, well‑reviewed API, you greatly reduce the impact of XSS while still allowing your app to perform privileged operations when needed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.