101

I'm trying to use Node modules (in this example, fs) in my renderer processes, like this:

// main_window.js
const fs = require('fs')

function action() {
    console.log(fs)
}

Note: The action function gets called when I press a button in my main_window.

But this gives an error:

Uncaught ReferenceError: require is not defined
    at main_window.js:1

I can solve this issue, as suggested by this accepted answer, by adding these lines to my main.js when initializing the main_window:

// main.js
main_window = new BrowserWindow({
    width: 650,
    height: 550,
    webPreferences: {
        nodeIntegration: true
    }
})

But, according to the docs, this isn't the best thing to do, and I should instead, create a preload.js file and load these Node modules there and then use it in all of my renderer processes. Like this:

main.js:

main_window = new BrowserWindow({
    width: 650,
    height: 550,
    webPreferences: {
        preload: path.join(app.getAppPath(), 'preload.js')
    }
})

preload.js:

const fs = require('fs')

window.test = function() {
    console.log(fs)
}

main_window.js:

function action() {
    window.test()
}

And it works!


Now my question is, isn't it counter-intuitive that I should write most of the code of my renderer processes in preload.js (Because only in preload.js I have access to Node modules) and then merely call the functions in each renderer.js file (for example here, main_window.js)? What am I not understanding here?

Amir Shabani
  • 3,857
  • 6
  • 30
  • 67

5 Answers5

140

Edit 2022


I've published a larger post on the history of Electron (how security has changed throughout Electron versions) and additional security considerations Electron developers can make to ensure the preload file is being used correctly in new apps.

Edit 2020


As another user asked, let me explain my answer below.

The proper way to use the preload.js in Electron is to expose whitelisted wrappers around any module your app may need to require.

Security-wise, it's dangerous to expose require, or anything you retrieve through the require call in your preload.js (see my comment here for more explanation why). This is especially true if your app loads remote content, which many do.

In order to do things right, you need to enable a lot of options on your BrowserWindow as I detail below. Setting these options forces your electron app to communicate via IPC (inter-process communication) and isolates the two environments from each other. Setting up your app like this allows you to validate anything that may be a require'd module in your backend, which is free from the client tampering with it.

Below, you will find a brief example of what I speak about and how it can look in your app. If you are starting fresh, I might suggest using secure-electron-template (which I am the author of) that has all of these security best-practices baked in from the get go when building an electron app.

This page also has good information on the architecture that's required when using the preload.js to make secure apps.


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) => func(...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>
reZach
  • 8,945
  • 12
  • 51
  • 97
  • 3
    If you have time you should move the basic idea from your comment at the link, cause right now this answer looks kind of just a boilerplate answer without answering the actual question. Especially if the link ever dies – Assimilater Apr 04 '20 at 07:45
  • 1
    Thank you @Assimilater, I've added more detail to my answer. – reZach May 09 '20 at 02:38
  • @Zac, Any ideas on how to removed receive event listeners in index.html, as they cause EventEmitter memory leak detected warning in React. – Ram Kumar May 30 '20 at 08:15
  • 1
    @RamKumar , if it's the ipcRenderer, you can do this: https://www.electronjs.org/docs/api/ipc-renderer#ipcrendererremovelistenerchannel-listener. There's also a remove all method you can use as well. Hope this helps! – reZach Jun 01 '20 at 23:02
  • As per documentation, contextBridge.exposeInMainWorld is still a experimental feature. – Fraddy Oct 11 '20 at 04:03
  • 4
    First, this answer is awesome. If you also are using typescript, you'll need to change preload to `preload.ts` and do something like: https://stackoverflow.com/questions/56457935/typescript-error-property-x-does-not-exist-on-type-window – David Rasch Oct 14 '20 at 18:06
  • @Zac, suppose I build an application **without** any external content (just nunjucks, for example). Then is it okay if I expose the whole node API using contextIsolation? Just curious to now if a preload script is required in this case... – RealZombs Mar 11 '21 at 09:27
  • @RealZombs if you aren't using any external content, you should be fine using the whole node API. – reZach Apr 27 '21 at 03:22
  • 1
    In your example you use `fs` in the main process, do you know if there would be any security risks to use it directly in preload? – Arkellys May 11 '21 at 18:42
  • @Arkellys I believe prototype pollution can happen if `contextIsolation` is false. Also its more secure that `sandbox` be set to true and you can't set `sandbox` to true if you use `require` in preload. – reZach May 12 '21 at 00:03
  • I used the same config as you for my windows, with `contextIsolation` sets to `true`. I tried to set `sandbox` to `true` as well but it made everything crash. I didn't know it was because of require though. Now I'm just trying to figure out if I really have to invoke the main process to do what I need with `fs` or if it's okay to use it in my preload file. ^^ – Arkellys May 12 '21 at 05:30
  • @Arkellys it's better to use `fs` in your main process, there's a rough outline how to do that here https://github.com/reZach/secure-electron-template/blob/master/docs/newtoelectron.md. `sandbox` to true will likely break things because it requires more upfront set up to work. – reZach May 12 '21 at 13:47
  • I will take a look, thank you for the help. – Arkellys May 20 '21 at 05:28
  • For anyone having an issue with `contextIsolation` and window functions in the `preload.js` file giving the error `window.functionName is not a function`, here is a useful link that helped me a lot: https://www.electronjs.org/docs/tutorial/context-isolation – avisk Jul 12 '21 at 00:18
  • 1
    If `path.join(__dirname, 'preload.js')` don't work then use `__static`. My problem and solution https://stackoverflow.com/questions/60814430/electron-builder-with-browserwindow-and-preload-js-unable-to-load-preload-scrip/68795261#68795261 – Андрей Сорока Aug 15 '21 at 21:01
  • when my app launches, i want to use `preload.js` to populate a table in a secondary or non-index html page, but obvs `preload.js` does not seem to have access to it. do u know how i would go about this or if this is even the appropriate way to do things? in other words, `index.html` has links that point to other `.html` files; it is one of these other `.html` files that has a `table` that i would like to populate before ever even navigating to that `.html` page from `index.hmtl` – oldboy Dec 19 '21 at 15:43
  • This answer is gold. How can I remove the listener in the preload / receive function after I called it? – Woww Mar 21 '22 at 13:13
  • @Woww there is a `.once` method that's only used once - https://www.electronjs.org/docs/latest/api/ipc-renderer/#ipcrendereroncechannel-listener – reZach Mar 22 '22 at 02:26
  • I receive the error `Uncaught Exception: ReferenceError: fs is not defined at IpcMainImpl.eval (webpack:///./src/background.js?:99:3) at IpcMainImpl.emit (events.js:315:20) at Object. (electron/js2c/browser_init.js:161:9492) at Object.emit (events.js:315:20)` as soon as the ipcRenderer gets called. I really don't know how to continue from here. My setup is electron with VueJS 3 and Vite as build tool – René Pardon Apr 16 '22 at 19:59
  • 1
    @reZach This worked for me. Thank you!! However, the console.log in `index.html` does not work. Please change it to alert or something like that. – Arundale Ramanathan Feb 01 '23 at 06:45
33

Consider this illustration

Electron Main Preload and Renderer

Not all those in the official documentation are directly implementable anywhere on your code. You must need a concise knowledge on the environments and processes.

Environment/Process Description
Main APIs that are much closer to the OS (low-level). These include the file system, OS-based notification popups, taskbar, etc. These were made possible through the combination of Electron's core APIs and Node.js
Preload A somewhat recent addendum in order to prevent powerful APIs available in the main environment from leaking. For more details, see Electron v12 changelogs and Issue #23506.
Renderer APIs of a modern web browser such as DOM and front-end JavaScript (high-level). This was made possible through Chromium.

Context isolation and Node integration

Scenario contextIsolation nodeIntegration Remarks
A false false Preload is not needed. Node.js is available in the Main but not in the Renderer.
B false true Preload is not needed. Node.js is available in the Main and Renderer.
C true false Preload is needed. Node.js is available in the Main and Preload but not in the Renderer. Default. Recommended.
D true true Preload is needed. Node.js is available in the Main, Preload, and Renderer.

How to use the preload properly?

You have to use Electron's inter-process communication (IPC) in order for the Main and the Renderer processes to communicate.

  1. In the Main process, use the:
  2. In the Preload process, expose user-defined endpoints to the Renderer process.
  3. In the Renderer process, use the exposed user-defined endpoints to:
    • send messages to Main
    • receive messages from Main

Example implementation

Main

/**
 * Sending messages to Renderer
 * `window` is an object which is an instance of `BrowserWindow`
 * `data` can be a boolean, number, string, object, or array
 */
window.webContents.send( 'custom-endpoint', data );

/**
 * Receiving messages from Renderer
 */
ipcMain.handle( 'custom-endpoint', async ( event, data ) => {
    console.log( data )
} )

Preload

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld( 'api', {
    send: ( channel, data ) => ipcRenderer.invoke( channel, data ),
    handle: ( channel, callable, event, data ) => ipcRenderer.on( channel, callable( event, data ) )
} )

Renderer

/**
 * Sending messages to Main
 * `data` can be a boolean, number, string, object, or array
 */
api.send( 'custom-endpoint', data )

/**
 * Receiving messages from Main
 */
api.handle( 'custom-endpoint', ( event, data ) => function( event, data ) {
    console.log( data )
}, event);

How about using promises?

As much as possible, keep promises to the same process/environment. Your promises on the main should stay on the main. Your promises on the renderer should also stay on the renderer. Don't make promises that jump from main-to-preload-to-renderer.

File system

Much of your business logic should still be in the Main or Renderer side but should never be in the Preload. This is because the Preload is just there pretty much as a medium. The Preload should be very minimal.

In OP's case, fs should be implemented on the Main side.

Abel Callejo
  • 13,779
  • 10
  • 69
  • 84
  • I would accept scenarios A & B configurations specifically for contextIsolation up to v12 of Electron . Since v12 and higher `contextIsolation : true` (default) . In case of contextIsolation set to false required , this can be achieved securely via contextBridge ; Coming to `nodeItegration` property : if preload scripts presented those ignore `nodeItegration` no matter what Boolean value it would be set to tho . – projektorius96 Nov 23 '21 at 20:59
  • when my app launches, i want to use `preload.js` to populate a table in a secondary or non-index html page, but obvs `preload.js` does not seem to have access to it. do u know how i would go about this or if this is even the appropriate way to do things? in other words, `index.html` has links that point to other `.html` files; it is one of these other `.html` files that has a `table` that i would like to populate before ever even navigating to that `.html` page from `index.hmtl` – oldboy Dec 19 '21 at 15:45
  • @oldboy in your case, you can do: (1) In Renderer, send a message to the main. (2) In Main, process the message, then send a message to the Renderer. (3) In Renderer, process the message, populate the table. The reason Preload does not have access to it is because Preload runs one-time only and it is before the Renderer execution. At that time it can no longer process real-time/incoming changes. – Abel Callejo Dec 20 '21 at 02:23
  • @AbelCallejo ahh its been so long since i coded with nodejs/electron that i wanted to avoid that xD i had decided in the meantime to just make it a SPA, but this still has issues cuz eles in the document fragments with which the table is populated by `preload` arent even detected by `renderer` in the DOM until after a somewhat significant delay :( thanks for the info <3 actually, one more Q: is`secondary.html` or `secondary.js` only executed once the link of `index.html` is clicked and `secondary.html` is visited/viewed in the `BrowserWindow`? – oldboy Dec 20 '21 at 09:00
12

Things have progressed quickly in Electron, causing some confusion. The latest idiomatic example (as best as I can determine after much gnashing of teeth) is:

main.js

app.whenReady().then(() => {`
    let mainWindow = new BrowserWindow({`
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            contextIsolation: true
        },
        width:640,
        height: 480,
        resizable: false
    })
 ... rest of code

preload.js

const { contextBridge, ipcRenderer} = require('electron')

contextBridge.exposeInMainWorld(
    'electron',
    {
        sendMessage: () => ipcRenderer.send('countdown-start')
    }
)

renderer.js

document.getElementById('start').addEventListener('click', _ => {
    window.electron.sendMessage()
})
cachique
  • 1,150
  • 1
  • 12
  • 16
N Smith
  • 179
  • 1
  • 5
  • 1
    I can't understand why putting nodeIntegration, etc... both in root config and in wwebPreferences... saved my life... But, you saved my life. Thank you! – Benoît Lahoz Dec 24 '20 at 16:03
2

I picked up Electron this week again and this was a tricky concept to get around to, but as I saw the reasoning it made perfect sense.

We live in an era where security is very important. Companies are being held to ransom, data is stolen. There are bad people everywhere. That's why you don't anyone to be able to execute code on your PC just because they happened to discover a vulnerability through your app.

So Electron is promoting good behavior by clamping down on it.

You can no longer access the system APIs from the render process, at least not the entirety of it. Only those bits that you expose to your render process through the preload file.

So write your UI code on the browser side, and expose functions inside of the preload.js file. Connect your render-side code to the main process using the ContextBridge

Using the exposeInMainWorld function of the context bridge.

Then inside of your render files, you can just refer to that function.

I can't say it's clean, but it works.

Cyril Gupta
  • 13,505
  • 11
  • 64
  • 87
1

I see you got a little off-topic answer, so...

Yes, you need to split your code into 2 parts:

  • event handling and displaying data (render.js)
  • data preparation / processing: (preload.js)

Zac gave an example of a mega-safe way: by sending messages. But the electron accepts your solution:

// preload.js

const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('nodeCrypto', require('./api/nodeCrypto'))
)


// api/nodeCrypto.js

const crypto = require('crypto')
const nodeCrypto = {
  sha256sum (data) {
    const hash = crypto.createHash('sha256')
    hash.update(data)
    return hash.digest('hex')
  }
}
module.exports = nodeCrypto 

Note that both approaches are requesting return data, or perform the operation. It is a mistake to directly host "native" Node libraries. Here is an example of "innocent" sharing of a logger. And it was enough to expose only selected methods using the proxy object.

In the same article is an example of the use of communication ipc does not relieve us from thinking... So remember to filter your input.

Finally, I will quote the official documentation:

Just enabling contextIsolation and using contextBridge does not automatically mean that everything you do is safe. For instance this code is unsafe.

// ❌ Bad code
contextBridge.exposeInMainWorld('myAPI', {
  send: ipcRenderer.send
})

// ✅ Good code
contextBridge.exposeInMainWorld('myAPI', {
  loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
bato3
  • 2,695
  • 1
  • 18
  • 26