Secure Electron IPC
In this post we’ll look at how to do secure IPC (Inter-Process-Communication) in Electron.
PLEASE NOTE this is NOT a tutorial on Electron or Electron IPC, it will assume you already know how to use Electron and how IPC works.
Why does it matter?
To keep it short, insecure IPC can lead to full blown RCE (Remote Code Execute).
How? If you misconfigure the following two settings:
contextIsolationset tofalsenodeIntegrationset totrue
With the above settings, it’s possible to do the following on the front-end in JavaScript:
1
2
3
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('send-ipc-msg-server', 'Hello');
- And this should work perfectly fine.
The RCE Problem
While the above solution might work for IPC, we’ve just opened the back-end up to the front-end.
If someone were to get XSS on the front-end, they could run a command like this:
1
window.require('child_process').execSync('calc');
For a little bit more fun:
1
window.require('fs').readdirSync('C:\\');
So yeah, this is not a good solution. Fortunately, there is a better way to do IPC.
Server Side
electron.js
Our first step will be to ensure BrowserWindow doesn’t have any insecure option enabled.
contextIsolationshould betrue.enableRemoteModuleshould befalse.
The second step will to specify the location of our preload.js file. If you don’t already have a preload.js file, create one and specify the path:
1
2
3
4
5
6
7
8
9
10
11
12
13
const electron = require('electron');
const BrowserWindow = electron.BrowserWindow;
const path = require('path');
const win = new BrowserWindow({
width: 1800,
height: 1000,
webPreferences: {
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, "preload.js")
}
});
Finally we will also need to setup an IPC listener on the back-side. This can be done in electron.js or another back-side file:
1
2
3
4
5
6
7
8
9
10
const { ipcMain } = require('electron');
// Event handler for incoming IPC messages
ipcMain.on('send-ipc-msg-server', (event, arg) => {
console.log('From CLIENT:');
console.log(arg);
// Send a message back to the client
event.sender.send('send-ipc-msg-client', `Return Message: ${(new Date()).toLocaleString()}`)
});
preload.js
In the preload.js file, the following code will be required:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld(
"ipcComm", {
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
on: (channel, callback) => {
// Remove `event` parameter (the underscore parameter) as the UI doesn't need it
ipcRenderer.on(channel, (_, ...args) => {
callback(...args);
});
},
removeAllListeners: (channel) => {
ipcRenderer.removeAllListeners(channel);
}
}
);
What does this do? This creates a safe “bridge” for our front-end to talk to the back-end. Any communication with the back-end must go through this bridge.
contextBridge.exposeInMainWorld(apiName, obj) takes 2 parameters:
apiNameis the key that will be added to the front-endwindowObject. You can access it usingwindow[apiName].objis the Object or Value that will be returned when you accesswindow[apiName].
The name ipcComm is arbitrary and you can call it what you like.
The keys inside the provided Object have been setup as follows:
senda function to send messages to the back-end on a certain channel.ona function to setup a listener on a channel for when the server sends messages back.removeAllListenersa function to remove all the listeners on a channel.
Why should you do it like this?
The reason for using this system is so that the front-end can’t import back-end modules directly, nor can it use the contextBridge or ipcRenderer modules directly.
Client Side
Now that we have our bridge setup, let’s see how to use it. In a Component let’s add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
useEffect(() => {
window.ipcComm.on('send-ipc-msg-client', (returnValue) => {
console.log("From SERVER:");
console.log(returnValue);
});
window.ipcComm.send('send-ipc-msg-server', 'Hello');
return () => {
window.ipcComm.removeAllListeners('send-ipc-msg-client');
}
// eslint-disable-next-line
}, [0]);
Break down of the code above:
Line 2-5setup a listener on thesend-ipc-msg-clientchannel with a callback for when a message is received.Line 7send the messageHelloto the server on thesend-ipc-msg-serverchannel.Line 9-11return a function that will remove all listeners on thesend-ipc-msg-clientchannel when the Component is unmounted.Line 12-13Provide a static value touseEffectso that the code is only run once when the Component is mounted and NOT after every update.
If you are using a class based Component, then you can setup the listener and send the message in componentDidMount, and remove the listener in componentWillUnmount:
1
2
3
4
5
6
7
8
9
10
11
12
componentDidMount() {
window.ipcComm.on('send-ipc-msg-client', (returnValue) => {
console.log("From SERVER:");
console.log(returnValue);
});
window.ipcComm.send('send-ipc-msg-server', 'Hello');
}
componentWillUnmount() {
window.ipcComm.removeAllListeners('send-ipc-msg-client');
}

