Securing Electron IPC with ContextBridge and the Invoke Pattern
Learn how to secure Electron applications using ContextBridge and the ipcRenderer.invoke pattern to prevent RCE vulnerabilities while maintaining a clean API.
27 Feb 2026, 04:35 UTC

The Risk of the Open Door
In Electron, the Renderer process is essentially a Chromium browser window. If you enable nodeIntegration, any JavaScript running in that window—including third-party libraries or remote content—gains full access to the Node.js API. This means a simple Cross-Site Scripting (XSS) vulnerability could allow an attacker to execute require('child_process').exec('rm -rf /') on a user's machine.
The solution is to keep the Renderer isolated and use a Preload script as a gated entry point. By combining contextIsolation with the ContextBridge API, you can expose a tiny, sanitized set of functions to the frontend without giving it the keys to the operating system.
The Architecture of a Safe Bridge
To implement a secure communication flow, you must configure your BrowserWindow to enforce isolation. This ensures the Preload script and the Renderer process run in separate JavaScript contexts, even though they share the same DOM.
The ContextBridge allows you to define a specific API that is injected into the window object of the Renderer. Instead of giving the Renderer the ipcRenderer module, you give it a wrapper function that only sends specific, pre-defined messages to the Main process.
Implementing the Invoke/Handle Pattern
Older Electron versions relied on send and on, which created fragmented, event-based code that was difficult to track. The modern standard is the ipcRenderer.invoke and ipcMain.handle pattern. This creates a request-response cycle using Promises, making asynchronous system calls feel like standard API requests.
Example: Secure File System Access
In this scenario, the Renderer needs to request the version of the application from the Main process. We will avoid exposing the entire IPC module.
1. Main Process (main.js)
Run this in the Node.js environment. This process has full system access.
const { app, BrowserWindow, ipcMain } = require('electron');
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true, // Essential for security
nodeIntegration: false, // Prevent direct Node access
preload: path.join(__dirname, 'preload.js')
}
});
// Handle the request from the renderer
ipcMain.handle('get-app-version', async () => {
return app.getVersion();
});
2. Preload Script (preload.js)
This script acts as the bridge. It has access to Node.js but exposes only a limited API to the window.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
getVersion: () => ipcRenderer.invoke('get-app-version')
});
3. Renderer Process (renderer.js)
This runs in the Chromium window. It cannot access require or ipcRenderer directly.
async function displayVersion() {
// Access the API exposed via ContextBridge
const version = await window.electronAPI.getVersion();
document.getElementById('version-display').innerText = `Version: ${version}`;
}
displayVersion();
Performance and Serialization Limits
While ContextBridge is secure, it introduces a serialization overhead. Every piece of data sent across the IPC bridge is serialized to JSON and deserialized on the other side. This has two practical implications:
- Complex Objects: You cannot pass functions, class instances, or DOM elements through the bridge. Only plain JavaScript objects, arrays, and primitives are supported.
- Data Volume: Sending massive buffers or multi-megabyte JSON strings frequently can freeze the Renderer's main thread, leading to "jank" in the UI.
Verification and Testing
To verify your security boundary is working, open the Chrome DevTools in your Electron app and attempt the following in the Console:
- Type
require('electron'): This should throw aReferenceError. - Type
window.ipcRenderer: This should beundefined. - Type
window.electronAPI.getVersion(): This should return a Promise that resolves to your app version.
If require is available in the console, your nodeIntegration is enabled or contextIsolation is disabled, and your application is vulnerable to remote code execution.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.