45

So, I've followed a number of guides to set up Webpack, Electron, and React to make a desktop application. After finishing the setup, I got to work, and learned that I needed to require an IPC mechanism from the main and renderer in order to communicate.

import {ipcRenderer} from "electron"; Adding this to my renderer.js file causes the error Uncaught ReferenceError: require is not defined.

After taking my problem to some colleagues, it was suggested that in my main.js file I should change

webPreferences: {
    nodeIntegration: false,
}

to

webPreferences: {
    nodeIntegration: true,
}

Everywhere I've read on google has said very clearly that if safety is something you care about, this is not something you should do. However, every resource I've been able to come across for electron ipc has used the ipcRenderer.

Now, does every example on the internet have huge security flaws, or am I missing some key part here?

My questions are as follows.

  1. Is it possible to use ipcRenderer without enabling nodeIntegration?
  2. If it is, how do I do it, and why would so many resources exclude this information?
  3. If it is not, what do I use?

If I'm asking the wrong question, or I missed something, or there are any other clear problems with the way I've asked this question please let me know, otherwise thanks in advance.

Antflga
  • 624
  • 1
  • 6
  • 9
  • 2
    `nodeIntegration` enables/disables the use of NodeJS and since Electron is a NodeJS module, you can't use it without NodeJS. So, no, if you want to use Electron's `ipcRenderer`, you will have to enable NodeJS. – Alexander Leithner Sep 09 '18 at 09:44
  • 1
    I agree. With `nodeIntergation` set to `false` one simply cannot communicate between `main` and `renderer` processes. I actually wonder what the real-world usage of electron will be for cases when it is set to `false`, now that they make it default. – jayarjo Aug 04 '19 at 18:06
  • 1
    @jayarjo i dont understand because according to the electron documentation `nodeIntegration` is set to `false` by default, so it must not be required? – oldboy Oct 24 '19 at 11:13
  • You can still communicate with the main thread via other means when `nodeIntegration` is disabled. For example, you can establish a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) connection or standard HTTP methods (e.g. `GET` / `POST` JSON in background). The answer below by Luke H provides detailed explanations and solutions for all 3 of your questions, and I recommend marking it as the accepted answer. – jacobq Dec 11 '19 at 19:38
  • In this [Q&A](https://stackoverflow.com/q/69605882/1244884) I posted an example on how to setup IPC-based communications between main and renderer processes via a preload script. Hope it helps. – customcommander Dec 13 '21 at 21:33
  • @customcommander, are some applications more vulnerable than others when allowing nodeintegration? Can it be allowed if some actions or functionalities are avoided? – Vass Jun 03 '22 at 14:09

1 Answers1

45
  1. Is it possible to use ipcRenderer without enabling nodeIntegration?

It is possible, but fiddly. It can be done by using a preload script.

  1. If it is, how do I do it, and why would so many resources exclude this information?

It is possible, using the preload script as indicated below. However, this is not considered secure. Most of the existing documentation does not show best security practices.

A more secure example is given afterwards.

// preload.js
const electron = require('electron');

process.once('loaded', () => {
  global.ipcRenderer = electron.ipcRenderer;
});
// main.js
const {app, BrowserWindow} = require('electron');

app.on('ready', () => {
  // Create the browser window.
  win = new BrowserWindow({
      backgroundColor: '#fff', // always set a bg color to enable font antialiasing!
      webPreferences: {
        preload: path.join(__dirname, './preload.js'),
        nodeIntegration: false,
        enableRemoteModule: false,
        // contextIsolation: true,
        // nativeWindowOpen: true,
        // sandbox: true,
      }
  });
  win.loadURL(`file://${path.join(__dirname, 'index.html')}`);

NOTE That the path to the preload script must be absolute and this can also get complicated when using webpack/babel, as the output file may be a different path.

  1. If it is not, what do I use?

Edit As @Yannic pointed out, there is now another option supported by Electron, called contextBridge. This new option may solve the problem more simply. For info on contextBridge, check the electron docs: https://www.electronjs.org/docs/tutorial/context-isolation

However, even with contextBridge you should not be try to expose entire electron APIs, just a limited API you have designed for your app

As mentioned, although it is possible to use ipcRenderer as shown above, the current electron security recommendations recommend also enabling contextIsolation. This will make the above approach unusable as you can no longer add data to the global scope.

The most secure recommendation, AFAIK is to use addEventListener and postMessage instead, and use the preload script as a bridge between the renderer and the main scripts.

// preload.js
const { ipcRenderer } = require('electron');

process.once('loaded', () => {
  window.addEventListener('message', event => {
    // do something with custom event
    const message = event.data;

    if (message.myTypeField === 'my-custom-message') {
      ipcRenderer.send('custom-message', message);
    }
  });
});
// main.js
const {app, ipcMain, BrowserWindow} = require('electron');

app.on('ready', () => {
  ipcMain.on('custom-message', (event, message) => {
    console.log('got an IPC message', e, message);
  });

  // Create the browser window.
  win = new BrowserWindow({
      backgroundColor: '#fff', // always set a bg color to enable font antialiasing!
      webPreferences: {
        preload: path.join(__dirname, './preload.js'),
        nodeIntegration: false,
        enableRemoteModule: false,
        contextIsolation: true,
        sandbox: true,
        // nativeWindowOpen: true,
      }
  });
  win.loadURL(`file://${path.join(__dirname, 'index.html')}`);
// renderer.js
window.postMessage({
  myTypeField: 'my-custom-message',
  someData: 123,
});
Luke H
  • 3,125
  • 27
  • 31
  • 6
    Note that this configuration is for maximum security, assuming that your app may open external URLs. You can use node integration with some security if you carefully lock down the use of external sites. – Luke H Sep 17 '19 at 02:05
  • So we should create a unique preload for each window? – Pitipaty Oct 29 '19 at 18:46
  • 1
    As far as I can see you'd be best using a single preload and basically creating a kind of API there. Again, if you're not loading/permitting external sites, you can probably skip some of this security. – Luke H Oct 30 '19 at 06:37
  • Can a similar approach be used to require() modules within the renderer process? @LukeH – Questionnaire Dec 30 '19 at 17:32
  • 3
    You can't actually use 'require' using this system, as the only communication via messages that are silently serialized and deserialized. This means you can't pass objects by identity (only a copy of the data is passed), and any special classes are lost. Essentially it's like doing JSON.parse(JSON.serialize(...)). You can only pass data. You basically need to implement any node-like logic in the main process, and use messages to send/receive plain data to the renderer process – Luke H Dec 31 '19 at 00:17
  • What is the advantage/difference of using `addEventListener`/`postMessage` instead of the `contextBridge` method, described in the [electron docs](https://www.electronjs.org/docs/tutorial/context-isolation#migration)? – Yannic Aug 17 '20 at 09:56
  • 1
    @Yannic This answer was written before `contextBridge` was available. So, this approach works for older electron versions. It presumably implements a similar method behind the scenes? – Luke H Sep 05 '20 at 10:07
  • Can you explain why does the postMessage solution work? You are sending a postMessage on the window, but if there's context isolation - the preload and renderer are not supposed to share window objects, so why is preload script window supposed to receive the message? – Maciej Krawczyk Mar 19 '21 at 11:42
  • 1
    @MaciejKrawczyk This is a good question. I haven't had time to dive into this. Since the preload is attached to a BrowserWindow, I expect that Electron is making sure the preload window object is correctly connected to this BrowserWindow. Worth investigating! I'm not positive that this answer is still relevant to modern electron. – Luke H May 13 '21 at 23:56
  • @Luke I have found the answer to this. The Electron docs are not doing a great job currently at explaining what contextIsolation is, it's not a complete isolation - e.g. the dom is still shared. More info can be found in this comment: https://github.com/electron/electron/issues/27024#issuecomment-745618327 – Maciej Krawczyk May 14 '21 at 08:16
  • @MaciejKrawczyk ah, great detective work! – Luke H May 16 '21 at 21:41
  • For the record, Electron 6 introduces the `contextBrige` API. For Electron 5, you can use the `global` approach as demonstrated in this answer – Eric Burel Aug 24 '21 at 08:25
  • Additional question: how do you debug who is triggering the `require` error? I have to update an old application, I suspect that some deprecated 3rd party lib might be responsible for using remote/ipcRenderer. Also, what are the best practices, because the API you expose might be big? I've added "preload" as an entry point of the "main" process webpack config, so at least it can use "normal" imports etc. Then I use the "window.electron" object only within a specific "renderer/api/" folder, where I define more abstracted services to communicate between renderer and main. – Eric Burel Aug 24 '21 at 09:17