Creating an Electron App - Secure core structure
Electron is split into a main process and a renderer process. The main process is a Node.js environment with full operating system access, whereas the renderer process does not have access to Node.js by default.
nodeIntegration: false
This strict division is done on purpose for security reasons. A somewhat reasonable scenario would be an XSS attack which can directly lead to a "Remote Code Execution" attack with full access to the user's computer.
But how can the main and renderer processes communicate then?
This is done using a so-called preload script, it has limited access to the require
function and (not by default!) access to the common window
object. This way one could expose a function from the preload script:
// preload.js without context isolation
const destroyComputer = require("destroy-computer");
window.myAPI = {
someFunction: (arg) => {
return {
foo: "bar",
test: destroyComputer
}
}
}
//renderer.js
window.myApi.someFunction();
This of course is bad because now it may happen that we punch a hole through the mentioned security by exposing a powerful api without any kind of filtering by mistake
contextIsolation: true
Context Isolation ensures that the preload
scripts is seperated from the website, this means the window
object of the preload script is not the same window
object that the renderer process has access to.
Great, but again: how can the main and renderer process communicate now?
We simply expose our api in a different way:
Looks about the same and accessing our api from the renderer
works exactly the same, but the very important difference is:
[...] values are proxied to the other context and all other values are copied and frozen. Any data / primitives sent in the API become immutable and updates on either side of the bridge do not result in an update on the other side.
from https://www.electronjs.org/docs/latest/api/context-bridge
This way the dangerous model / function is not accessible in the renderer world!
Inter-Process Communication
The last puzzle-piece we need is the IPC, since renderer and preload can now communicate safely through the exposed API but main and renderer can not, we need a way to communicate between preload and main
This is done by using the ipcMain
and ipcRenderer
modules. The name "ipcRenderer" is a bit misleading in our case since the module is not available in the renderer process, we use it in the preload script.
//receive in main from preload
ipcMain.on("example", () => {
//do something
});
//send from main to preload
ipcMain.send("example")
Thats it! Check out the link above for a more in-depth example of a renderer-to-main communication