15

I am stuck when creating custom window controls like close, min/max and restore with nodeIntegration turned off. I created the buttons in my renderer's local html file

main.js

mainWindow = new BrowserWindow({
    x, y, width, height,
    frame: false,
    show: false,
    webPreferences: { devTools: true }
});

mainWindow.loadURL(url.format({
    protocol: 'file:',
    slashes: true,
    pathname: path.join(__dirname, 'assets', 'index.html')
}));

index.html

<div id='minimize' class='noSelect'>&#xE921;</div>
<div id='maximize' class='noSelect'>&#xE922;</div>
<div id='restore' class='noSelect'>&#xE923;</div>
<div id='close' class='noSelect'>&#xE8BB;</div>

<script type='text/javascript' src='../assets/js/index.js'></script>

By default, nodeIntegration is off so index.js has no access to Node. However, I need to be able to add functionality to the buttons to close, min/max and restore the window.

index.js

const { remote } = require('electron');
const mainWindow = remote.getCurrentWindow();

document.getElementById('close').addEventListener('click', () => {
  mainWindow.close();
});

This wouldn't work because of nodeIntegration being disabled. Is it safe to have it enabled in a local page? If not, what is a safe way of doing this?

AfterShotzZHD
  • 311
  • 2
  • 4
  • 15

3 Answers3

31

TL;DR: Enabling nodeIntegration only imposes risks if you load and execute code from untrusted sources, i.e. the internet or from user input.

If you are completely sure that your application will only run the code you have created (and no NodeJS module loads scripts from the internet), basically, there is no to very little risk if enabling nodeIntegration.

However, if you allow the user to run code (i.e. input and then eval it) or you provide plug-in APIs from which you do not have any control over the plug-ins loaded, the risk level rises because NodeJS allows any NodeJS script, ex., to manipulate the filesystem.

On the other hand, if you disable nodeIntegration, you have no way of communicating with the main process or manipulating the BrowserWindow, thus cannot create custom window controls. However, you can use a "preload" script file to build a bridge between the completely isolated renderer and the NodeJS world.

This is done by creating a script file which is then passed to the BrowserWindow as the preload: configuration option upon creation. Electron's documentation has some examples to get you started. Also, it's a good idea to familiarise yourself with Eelectron's security recommendations.

Alexander Leithner
  • 3,169
  • 22
  • 33
  • It will only be running code that I put in apart from a few modules from NPM. Not sure if that is what you meant about modules and plug-ins. The user will not be inputing code to be run, only rarely string values. – AfterShotzZHD Aug 15 '19 at 16:03
  • 1
    If you are sure that none of the NPM modules will load code from the internet, then it it is low-risk. – Alexander Leithner Aug 15 '19 at 17:11
  • Not sure how to check that. It's modules like mixer/client-node, ws, dotenv, mongodb. – AfterShotzZHD Aug 15 '19 at 18:07
  • Basically, it's up to you if you trust the vendors of the modules. The modules you stated look good to me so far, but the decision is a security consideration which is in your responsibility. – Alexander Leithner Aug 16 '19 at 11:41
  • 2
    What about the `preload` script angle? Would that work without enabling nodeIntegration? – AfterShotzZHD Aug 17 '19 at 04:16
  • Basically, yes -- you could define a function the window control buttons in the window will use, just like [the preload example](https://electronjs.org/docs/all#preload) demonstrates. – Alexander Leithner Aug 17 '19 at 11:18
  • 3
    This is a good answer. For an example of IPC with `nodeIntegration` disabled, see https://stackoverflow.com/a/57656281/289203 – Luke H Aug 26 '19 at 10:43
9

Keep in mind, that in year 2021 you do not need nodeIntegration to communicate with the main process from the renderer process.

Instead, you use message passing, like this:
main.js

const {app, BrowserWindow, ipcMain} = require("electron");
const path = require("path");

app.whenReady().then(open_window);

function open_window() {
  // Explain: Create app window.
  const appWin = new BrowserWindow({
    width: 800,
    height: 600,
    opacity: 0.5,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },

  // Explain: Render the app.
  void minisWindow.loadFile("index.html");
  
  // Spec: User can hide App window by clicking the button.
  ipcMain.on("hide-me", () => appWin.minimize());
  
  // Spec-start: When User focuses App window - it becomes fully opaque.
  ipcMain.on("make-window-opaque", () => appWin.setOpacity(1));
  appWin.on("show", () => minisWindow.setOpacity(1));
  appWin.on("blur", () => minisWindow.setOpacity(0.5));
  // Spec-end.
}

preload.js

const {ipcRenderer} = require("electron");

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener("DOMContentLoaded", () => {
  // Spec: User can hide App window by clicking the button.
  document.querySelector("#hideBtn").addEventListener("click", 
    () => ipcRenderer.send("hide-me"));
  });

  // Spec: When User focuses App window - it becomes fully opaque.
  document.body.addEventListener("click", () => ipcRenderer.send("make-window-opaque"));
});

This example illustrates two instances of message passing:

  1. When User clicks the #hideBtn button - a message is dispatched that instructs main to hide the window.
  2. By default the window is half-transparent; when User clicks on the window (essentially, activating the clickz event on the body) - a message is dispatched that instructs main to make the window fully opaque.
avalanche1
  • 3,154
  • 1
  • 31
  • 38
  • 3
    IMHO, this is not a good practice. You are now burying a responsibility that belongs to the renderer (handling onClick) in preload.js. One should use contextBridge to expose an api into the renderer. The renderer handles onClick, calls the api provided by contextBridge and preload/main executes the insecure code. – passerby Oct 27 '21 at 09:05
  • 1
    That is a thing of personal preference for the architecture. Me, for example, I don't like relying on globals like `window.electon`. Also I lose all the type-based goodies coming from `require`ing objects. – avalanche1 Nov 01 '21 at 17:45
  • I'm using typescript and added typings to the api provided by contextBridge. – passerby Nov 01 '21 at 18:08
  • Oh, nice! Can you send a link where I can read how to add TS to Electron? – avalanche1 Nov 01 '21 at 18:12
  • 1
    I got the idea for typings from here: https://github.com/frederiksen/angular-electron-boilerplate. See src/window-interface/index.d.ts – passerby Nov 01 '21 at 18:46
  • Looks more like stubbing, than typings – avalanche1 Nov 01 '21 at 18:58
  • @avalanche1 Does it still stand though that code not running/eval from the net or files outside of those provided should be safe without contextisolation and nodeintegration? – Vass Jun 03 '22 at 14:05
0

you don't need nodeIntegration since it delivers security issues, Instead use preload.js file

Why ?

-> Electron's main process (main.js) is a Node.js environment that has full OS access, renderer process (renderer.js) renders web pages not node.js because anyone can take data and can see the functionalities.

-> To bridge electron's different process types together, we need special script called 'preload.js'

-> preload script are injected before a webpage loads in the renderer.

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 20 '22 at 10:46