Written by Andres Acevedo
In our previous article, we explored how to create a useful Electron app focusing on functionality, but leaving aside some aspects like security and platform specific features.
In this article, we will continue from where we left, to progress in the path from a Prototype to a real world Application!
I’d like to begin with an analogy: in healthcare studies, there's an important Latin maxim: Primum non nocere, it means:
"first, do no harm". Another way to state it is that, "given an existing problem, it may be better not to do something, or even to do nothing, than to risk causing more harm than good."
If we are introducing security problems to our clients, it could be best to go back a bit (pun intended) and solve these vulnerabilities before adding new features to our pretty piece of software.
Working according to the warnings
So, is our app secure? If we run it with npx electron
. and open the Developer Tools (View>Toggle Developer Tools), we can see that the console is giving us several security warnings:
One easy way to improve our app's security is to add a special meta header to our index.html file:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
This instructs the Chromium rendering engine to only run local scripts.
You can also add other allowed domains in the following way:
Content-Security-Policy: script-src 'self' https://apis.example.com
If you want to learn more about CSP, check:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Once we add that line to the header section of our website and refresh our app (View > Reload)
, we are left with just one security warning:
Getting rid of this security warning requires that we modify our main.js
file and add the following webPreference
to our BrowserWindow
call:
webPreferences: {
worldSafeExecuteJavaScript: true,
contextIsolation: true
}
What this does is to enforce isolation between the Renderer World
(Chromium renderer) and Node World. This sandboxes any malicious code run from the renderer, reducing the possible damage of an exploited vulnerability.
Once we add the code, reloading the view is not enough - as we did changes in our main.js
file, so we have to terminate our Electron App and relaunch it with: npx electron
.
Good news: we got rid of the warning.
Bad news: now we have an actual error and our app does not work anymore
Fixing the code
Require
is a node function, and because we configured electron to separate renderer
and node worlds, we can not use require()
on our renderer.js
file.
Even if we have the nodeIntegration: true
setting.
In fact, let's remove that setting, so our BrowserWindow
call in main.js
now looks like this:
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
worldSafeExecuteJavaScript: true,
contextIsolation: true,
}
})
Electron provides us with a way to use node functions in the renderer world: Preload scripts.
Add the following line to webPreferences
in our BrowserWindow
call on main.js
.
preload: path.join(app.getAppPath(), 'preload.js')
It should look like this:
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
worldSafeExecuteJavaScript: true,
contextIsolation: true,
preload: path.join(app.getAppPath(), 'preload.js')
}
})
Don't forget to include the path module at the beginning of main.js
:
const path = require('path');
Now let's create the preload.js
file and write the following on it:
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld(
"electron",
{
ipcRenderer: ipcRenderer
}
);
As you can see on the first line, we can use the require()
function in preload.js
without problems in order to import the ipcRenderer
.
Now, we will remove the require line that is causing the error on the renderer.js
file:
const { ipcRenderer } = require('electron');
As we are not requiring ipcRenderer
anymore, we can not continue using it in the same way as before.
But that's when the contextBridge.exposeInMainWorld
function we added on preload.js
comes in handy.
Our ipcRenderer
can be accessed on the rendered.js
file by using window.electron.ipcRenderer
.
So it now should look like this:
async function fileSelected(e){
const loadedFilePath = e.target.files[0]?.path
let data = await window.electron.ipcRenderer.invoke('read-file', loadedFilePath)
document.getElementById("loadedText").value = data
}
document.getElementById("fileLoader").addEventListener("change", fileSelected);
Result:
We've made it! Our app works again and without any security warnings!
You can find the code of our Secure Text Loader on the following repo:
https://github.com/mran3/Secure-Text-File-Loader/
Top comments (0)