How to Use Preload.Js Properly in Electron

How to use preload.js properly in Electron

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>

nodeIntegration vs preload.js vs IPC in Electron

The code you pasted from the Context Isolation docu shows how you could use methods and modules exposed (via preload) to the renderer in earlierer versions of electron, before context isolation was set to true by default. This is however not how you should do it. With contextisolation = true you need to use the contextbridge in you preload, because the window objects are kept isolated.

So the code you pasted from IPC is the way you should do it in 2022. I'd also keep my fingers off NodeIntegration, unless you really know what you're doing.

Concerning your last question: Generally I'd follow a least-privilege approach. The less power you give to the renderer, the safer.

I'll give you one example from my recent project.
I need to make screenshots and then save them somewhere. For that I need Browserwindow.webcontents.capturePage() and fs.writeFile() which are both only available to the main process. It would definitely be risky to expose the fs.writeFile method to the renderer so I'm not doing that.
Instead I keep my logic for writing the screenshots to my filesystem in the main process. However I want to initiate the Screenshots from the renderer (my UI). To expose as little as possible I only expose a function that calls the invoke method in preload's contextBridge that looks like this:

contextBridge.exposeInMainWorld("ipcRenderer", {
invokeSnap: async (optionsString: string) =>
await ipcRenderer.invoke("snap", optionsString),
});

In main I have a listener that handles the incoming request:

ipcMain.handle("snap", async (_event, props: string) => {
// screenshot logic
return //sth that the renderer will receive back once the process is finished;
});

The renderer sends the the request and handles the response or errors like that:

window.ipcRenderer.invokeOpen(JSON.stringify(options))
.then(...)
.catch(...)

As a sideeffect this puts the heavy weight on the main process, which might not be desirable in bigger projects - idk. Maybe a more experienced electron developer can say more about that.

Electron - preload.js is not loaded and error is occurred on Windows 10

I was able to solve this problem oneself.

This problem wasn't in electron, it was in my Parallels Desktop for Mac environment.

I was trying to get the Electron working directory to work on Windows 10 through the Parallels network drive, but it doesn't seem to recognize the build file path properly.

Currently, even if you have a local install of Electron instead of a global install, it doesn't seem to be able to build properly.

npm install electron@latest --save-dev

It seems that a project that could be built on a macOS cannot be built directly on Windows 10.

Electron is "electron ." command to build on Windows 10, it seems to extract the necessary files on the C:\Users\[UserName]\AppData\Roaming\npm\ path. I don't know the exact reason, but it seems to be inconsistent with locally installed Electron.

If you want to run your project on Windows 10, copy the project directory and input "npm install" command to reinstall Node modules such as Electron. It will automatically reinstall the dependent packages listed in package.json.

As a result, I can finally build "node_modules/.bin/electron ." to build it without any problems.



Related Topics



Leave a reply



Submit