Securing Your Electron App

Securing Your Electron App

How to secure your desktop electron app

By Andres Acevedo, Published 2020-08-18

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:

electron showing 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:

electron showing only 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 .

electron showing console errors

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:

electron showing console

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/