How to use preload.js properly in Electron
Asked Answered
N

6

103

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?

Nadiya answered 5/9, 2019 at 14:14 Comment(1)
The preload script concept in Electron seems like a design flaw. Seeing many people write boilerplate code to allow render processes to send arbitrary events to the main process makes me think preload scripts should not be required at all. The documentation mentions repeatedly that we wouldn't want renderer processes to have access to things like ipcRenderer, but doesn't make it clear why.Nonu
B
143

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>
Bradski answered 19/1, 2020 at 20:32 Comment(24)
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 diesAlliteration
Thank you @Assimilater, I've added more detail to my answer.Bradski
@Zac, Any ideas on how to removed receive event listeners in index.html, as they cause EventEmitter memory leak detected warning in React.Ar
@RamKumar , if it's the ipcRenderer, you can do this: electronjs.org/docs/api/…. There's also a remove all method you can use as well. Hope this helps!Bradski
As per documentation, contextBridge.exposeInMainWorld is still a experimental feature.Homicide
First, this answer is awesome. If you also are using typescript, you'll need to change preload to preload.ts and do something like: #56458435Chapnick
@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...Stull
@Stull if you aren't using any external content, you should be fine using the whole node API.Bradski
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?Bollinger
@Bollinger 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.Bradski
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. ^^Bollinger
@Bollinger it's better to use fs in your main process, there's a rough outline how to do that here github.com/reZach/secure-electron-template/blob/master/docs/…. sandbox to true will likely break things because it requires more upfront set up to work.Bradski
I will take a look, thank you for the help.Bollinger
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: electronjs.org/docs/tutorial/context-isolationBreadroot
If path.join(__dirname, 'preload.js') don't work then use __static. My problem and solution #60814930Rabbin
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.hmtlLudovika
This answer is gold. How can I remove the listener in the preload / receive function after I called it?Confab
@Confab there is a .once method that's only used once - electronjs.org/docs/latest/api/ipc-renderer/…Bradski
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.<anonymous> (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 toolUnlettered
@Bradski 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.Ovariectomy
Why did the Electron team decide contextBridge.exposeInMainWorld was better than something like contextBridge.addToWindowObject? Would be a lot better to be explicitNonu
Another thing - the security example doesn't make sense. If there's going to be a way for the frontend to send some message to the main process, exposing a way to send arbitrary messages will make no difference. Whitelisting the frontend's possible IPC messages makes no sense because they will all be exposed regardless. no?Nonu
@MarcosPereira it may be due to the terminology chrome uses which is why they chose "exposeInMainWorld" over "addToWindowObject". By whitelisting specific endpoints in the front-end, the backend can code directly to these endpoints instead of allowing a more generic way for the front-end to send any message/parameters to the backend. I see this a somewhat similar to the concept of least-privilege.Bradski
@Bradski But there's no way to fine tune privileges for accessing the window object, so all IPC communication events are always accessible. The only bottleneck is the event handler code in main process, nothing else - exposing events directly or through a generic send method is the same.Nonu
L
34

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.

Legatee answered 10/11, 2021 at 17:27 Comment(4)
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 .Geotropism
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.hmtlLudovika
@Ludovika 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.Legatee
@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: issecondary.html or secondary.js only executed once the link of index.html is clicked and secondary.html is visited/viewed in the BrowserWindow?Ludovika
C
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()
})
Chloroform answered 10/12, 2020 at 21:46 Comment(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!Merrymerryandrew
S
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')
})
Sprinkling answered 1/7, 2021 at 13:22 Comment(3)
Why is that code unsafe? If you're going to expose all possible IPC calls as a method in window.myAPI, why not just expose a general "send" method?Nonu
@MarcosPereira this example is from official documentation. > "It directly exposes a powerful API without any kind of argument filtering. This would allow any website to send arbitrary IPC messages, which you do not want to be possible. The correct way to expose IPC-based APIs would instead be to provide one method per IPC message."Sprinkling
I find the documentation to be lacking in justification as well. If you're going to expose an endpoint for all IPC messages, a generic send method does not introduce any additional API surface. The argument filtering should happen in the destination method anyway, and hopefully not in the IPC handlers as that spreads the logic between more than one place.Nonu
W
1

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.

Weswesa answered 3/7, 2021 at 16:15 Comment(0)
S
1

@reZach and @Abel gave perfect answers. And if anyone needs this, here is what works well in my local. just a case for me and not promising it will fix your problem. Main point is how you use these node function in your bussiness code.

  1. add handle function in your main.js like below:
   async function handleFileOpen(type, arg) {
        return await dialog(null, { properties:[], ...arg});
 }
 ipcMain.handle('dialog:showOpenDialog', async (event, arg) => {
     return await handleFileOpen('showOpenDialog', arg);
}

this is how you use node api/function which you need in main process 3. expose them to renderer in preload.js using contextBridge:

const handler =  {
    dialog: {
       showOpenDialog:(options) => {
     return ipcRenderer.invoke('dialog:showOpenDialog', options);
}
  }
}
contextBridge.exposeInMainWorld('electron', handler);
export type ElectronHandler = typeof handler;
  1. in your code to use it:
window.electron.dialog.showOpenDialog({properties: [xxx]}).then();
Silverweed answered 26/3 at 7:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.