Secure Renderer‑Main Communication in Electron Using contextBridge
Learn how Electron’s context isolation and contextBridge let you expose safe, limited APIs from the preload script to the renderer, keeping Node access locked down.
21 Jul 2025, 22:08 UTC

The Problem: Keeping Renderer Code Safe While Still Needing Node Features
When building a desktop app with Electron, the renderer process loads web content that may come from untrusted sources. If the renderer could call Node.js APIs directly, a compromised web page could read files, spawn processes, or otherwise harm the user’s machine. Developers still need a way for the UI to request privileged operations—such as saving a file or accessing hardware—without giving the renderer unrestricted Node access.
How Context Isolation Works
Electron runs each renderer page in its own JavaScript context when contextIsolation is enabled (the default since Electron 12). In this mode, the page’s global objects (window, document, etc.) are separate from the preload script’s context. Consequently, code in the renderer cannot reach require, process, or any Node built‑ins that live in the preload or main process unless they are explicitly exposed.
Exposing a Controlled API with contextBridge
The contextBridge.exposeInMainWorld method lets a preload script whitelist specific functions or objects and attach them to the renderer’s window object. The renderer sees them as ordinary globals, but each call actually crosses the context boundary and executes in the preload’s privileged environment. Because only the listed members are exposed, the attack surface stays small.
Worked Example: A Simple Ping Service
- Create a folder
electron-pingand initializenpm init -y. - Install Electron:
npm install --save-dev electron. - Add
main.js:
const { app, BrowserWindow } = require('electron');
const path = require('node:path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
- Add
preload.js:
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('api', {
ping: () => {
// This runs in the preload context, where Node is available
return 'pong';
}
});
- Add
index.html:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Ping Test</title></head>
<body>
<h1>Electron contextBridge demo</h1>
<button id="pingBtn">Ping</button>
<pre id="output"></pre>
<script>
document.getElementById('pingBtn').addEventListener('click', async () => {
try {
const result = window.api.ping(); // exposed via contextBridge
document.getElementById('output').textContent = result;
} catch (e) {
document.getElementById('output').textContent = `Error: ${e}`;
}
});
</script>
</body>
</html>
- Run the app:
npx electron .. - Click the button; the renderer receives
pong. - Open DevTools and try
window.requireorwindow.process; they will beundefinedor throw, confirming isolation.
Trade‑offs and Limitations
- Exposure granularity: Developers must carefully decide which functions to expose. Exposing a powerful object like
child_processor the wholerequirere‑introduces the same risks that context isolation aims to mitigate. - Boilerplate: Every privileged operation needs a wrapper in the preload script, which can increase code size and require maintenance as the API evolves.
- Debugging visibility: Because the renderer and preload run in separate contexts, console logs from the preload appear in a different DevTools context; developers need to select the correct context to view them.
Actionable Closing
To reap the security benefits of Electron’s default configuration:
- Leave
nodeIntegrationat its default (false) and keepcontextIsolationenabled. - Place all privileged logic in a preload script and expose only the minimal set of functions via
contextBridge.exposeInMainWorld. - Validate any data received from the renderer before acting on it in the preload or main process.
- Periodically audit the exposed API surface to ensure no unnecessary power has been added.
Following this pattern lets you build feature‑rich desktop apps while keeping the renderer sandboxed against malicious web content.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.