How to use preload.js properly in Electron How to use preload.js properly in Electron node.js node.js

How to use preload.js properly in Electron


Edit

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 objectcontextBridge.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>


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()})


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.jsconst { contextBridge } = require('electron')contextBridge.exposeInMainWorld('nodeCrypto', require('./api/nodeCrypto')))// api/nodeCrypto.jsconst 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 codecontextBridge.exposeInMainWorld('myAPI', {  send: ipcRenderer.send})// ✅ Good codecontextBridge.exposeInMainWorld('myAPI', {  loadPreferences: () => ipcRenderer.invoke('load-prefs')})