Creating an Electron App - Secure core structure

Creating an Electron App - Secure core structure
Photo by Norbert Kowalczyk / Unsplash

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:

// preload with contextIsolation enabled
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
  someFunction: (arg) => {
    return {
      foo: "bar",
      test: destroyComputer
    }
  }
})
preload.js

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")
Inter-Process Communication | Electron
Use the ipcMain and ipcRenderer modules to communicate between Electron processes
Some examples

Thats it! Check out the link above for a more in-depth example of a renderer-to-main communication