Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Created April 2, 2020 20:04
Show Gist options
  • Save QNimbus/5c9bc53b12927232f20e176d172aae48 to your computer and use it in GitHub Desktop.
Save QNimbus/5c9bc53b12927232f20e176d172aae48 to your computer and use it in GitHub Desktop.

Source: electron/electron#9920 (comment)

I hope that this comment get noticed, because a lot of people are asking about importing fs or ipcRenderer in your apps. It's a common-need for electron apps but I found not many people have got it right, and are using outdated patterns. tl;dr - there is a security vulnerability if you don't import your node module (ie. fs) or electron module (ie. ipcRenderer) in the correct way. If you are using your app for only yourself you are probably safe, but if you ever want to share or sell your app you should read ahead.

Our goal

Before I go into the solution, it's important to understand why we are doing this in the first place. Electron apps allow us to include node modules in our apps, which gives them amazing power, but security concerns. We want to allow our app to use native-os (ie. node) features, but we don't want them to be abused.

As brought up by @raddevus in a comment, this is necessary when loading remote content. If your electron app is entirely offline/local, then you are probably okay simply turning on nodeIntegration:true. I still would, however, opt to keep nodeIntegration:false to act as a safeguard for accidental/malicious users using your app, and prevent any possible malware that might ever get installed on your machine from interacting with your electron app and using the nodeIntegration:true attack vector (incredibly rare, but could happen)!

The easy way

Setting nodeIntegration: true in your BrowserWindow gives your renderer process access to node modules. Doing this, is vulnerable. You have access to require("fs") and require("electron"), but that means if someone were to find a XSS vulnerability, they could run any command you've exposed in your renderer process.

Think deleting all of the files on your computer, or something else that's really bad.

The (alternative) easy way

Alongside setting nodeIntegration to true, it's likely that your app is using webpack to bundle application files. Webpack messes up with certain symbols, so settings like target: 'electron-renderer' or webpack externals allows you to pass through these variables (ipcRenderer) into your app instead.

Still, this changes nothing except how you are setting up your app.

The (other-alternative) easy way

You can use the remote module which gives you access to ipcRenderer. It's basically 'The easy way' in a different form. It's not recommended by Electron's security recommendations to do this since this type of attack suffers from a prototype pollution vector.

Ie. using remote could allow someone to modify a js-object's prototype and wreck havoc on your machine/app.

The almost right way

@marksyzm has a better solution, although not perfect, where we use IPC to send the ipcRenderer to the renderer process. This type of setup is also vulnerable to prototype pollution attacks. If you want to get your app 80% of the way there, I'd use this method, as it probably won't require you to do much refactoring.

The right way

The right way of importing your fs/ipcRenderer into your renderer process is with IPC (inter-process-communication). This is Electron's way of allowing you to talk between main and renderer process. Broken down, this is how your app needs to look:

  1. A BrowserWindow has a preload property. This property is a js file that loads with access to require (which means you can require ipcRenderer)
  2. Your BrowserWindow will also have contextIsolation: true to prevent prototype pollution attacks, but this means you need to use the contextBridge to pass the ipcRenderer to your renderer process
  3. Using the preload script and the contextBridge, you allow your renderer process to access the ipcRenderer
  4. In your main script, you create listeners for the ipcRenderer (in the ipcMain module). Within these listeners you can use the fs module

Roughly this is what all these steps look like:

main.js

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

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

async function createWindow() {

  // Create the browser window.
  win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false, // is default value after Electron v5
      contextIsolation: true, // protect against prototype pollution
      enableRemoteModule: false, // turn off remote
      preload: path.join(__dirname, "preload.js") // use a preload script
    }
  });

  // Load app
  win.loadFile(path.join(__dirname, "dist/index.html"));

  // rest of code..
}

app.on("ready", createWindow);

ipcMain.on("toMain", (event, args) => {
  fs.readFile("path/to/file", (error, data) => {
    // Do something with file contents

    // Send result back to renderer process
    win.webContents.send("fromMain", responseObj);
  });
});

preload.js

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

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        send: (channel, data) => {
            // whitelist channels
            let validChannels = ["toMain"];
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, data);
            }
        },
        receive: (channel, func) => {
            let validChannels = ["fromMain"];
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender` 
                ipcRenderer.on(channel, (event, ...args) => fn(...args));
            }
        }
    }
);

index.html

<!doctype html>
<html lang="en-US">
<head>
    <meta charset="utf-8"/>
    <title>Title</title>
</head>
<body>
    <script>
        window.api.receive("fromMain", (data) => {
            console.log(`Received ${data} from main process`);
        });
        window.api.send("toMain", "some data");
    </script>
</body>
</html>

At the very least, I believe you need electron v7 for these features.

How do I know this?

I care about secure electron apps, and built secure-electron-template in order to create an electron application template to bake-in security instead of thinking of security as an afterthought.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment