17

Here's my setup:

Step 1. Create a preload.js file with the code:

window.ipcRenderer = require('electron').ipcRenderer;

Step 2. Preload this file in your main.js via webPreferences:

  mainWindow = new BrowserWindow({
    width: 800, 
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      preload: __dirname + '/preload.js'
    }
  });

Step 3. In a renderer:

console.log(window.ipcRenderer); // Works!

Now following Electron's security guide, I wish to turn contextIsolation=true: https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content

Step 2bis.

  mainWindow = new BrowserWindow({
    width: 800, 
    height: 600,
    webPreferences: {
      contextIsolation: true,
      nodeIntegration: false,
      preload: __dirname + '/preload.js'
    }
  });

Step 3bis. In a renderer:

console.log(window.ipcRenderer); // undefined

Question: can I use ipcRenderer when contextIsolation=true?

jeanpaul62
  • 9,451
  • 13
  • 54
  • 94

3 Answers3

29

2022 edit

For those that would like additional context of understanding the IPC renderer, I've published a post that explains this concept, as well as related security concerns that revolve around using the IPC renderer.

new answer

You can follow the setup outlined here. This setup is being used in secure-electron-template Essentially, this is what you can do:

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>

original

You are still able to use ipcRenderer in a renderer process with contextIsolation set to true. The contextBridge is what you want to use, although there is a current bug that is preventing from you calling ipcRenderer.on in a renderer process; all you can do is send from the renderer process to the main process.

This code is taken from secure-electron-template a template for Electron built with security in mind. (I am the author)

preload.js

const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld(
    "electron",
    {
        ipcRenderer: ipcRenderer
    }
);

main.js

let win;

async function createWindow() {

  // Create the browser window.
  win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      nodeIntegrationInSubFrames: false,
      contextIsolation: true,
      enableRemoteModule: false,
      preload: path.join(__dirname, "preload.js")
    }
  });
}

some renderer.js file

window.electron.ipcRenderer

reZach
  • 8,945
  • 12
  • 51
  • 97
  • This is still insecure. Even with a contextBridge, exposing the entirety of the ipcRenderer as a method to the renderer can lead to abuse: https://www.electronjs.org/docs/tutorial/context-isolation#security-considerations – user1679669 Oct 19 '20 at 11:48
  • 2
    @user1679669 We are exposing a _subset_ of allowed operations on ipcRenderer, which isn't the same as the link you posted. Please see the **new answer** section for a better example of what you can do. – reZach Oct 19 '20 at 18:20
  • @reZach window.api.send is not accessible in vue component onmounted function, how can use it there ? – Saqib Shehzad Oct 24 '22 at 09:18
1

Notice this sentence in the middle of the description of context isolation. It's easy to miss.

The Electron API will only be available in the preload script and not the loaded page.

Looks like the answer is no.

pushkin
  • 9,575
  • 15
  • 51
  • 95
  • Do you know how can the main and renderer processes communicate in this case? – jeanpaul62 Apr 06 '19 at 19:03
  • @amaurymartiny Based on my testing, Node gets totally disabled in the renderer (even if there's no `webview`), and I can't require `ipcRenderer`, so it doesn't look like renderer can talk to main. That being said, it's bit strange that that would be the behavior, since I thought it would only affect the webview/preload and is the reason why I posted [this](https://github.com/electron/electron/issues/10405) issue. – pushkin Apr 23 '19 at 15:00
  • @amaurymartiny [This](https://github.com/electron/electron/issues/11608#issuecomment-367508850) could help. Looks like Electron APIs are available in the preload and in main. So you should still be able to send messages from preload to main, just not from the webview/preload renderer to the other renderer "wrapping" it – pushkin Apr 23 '19 at 20:42
0

Please check this. It works for me. I am using CRA & Electron.

preload.js


    const { contextBridge, ipcRenderer } = require('electron');
    const MESSAGE_TYPES = ipcRenderer.sendSync('GET_MESSAGE_TYPES');
    
    require = null;
    
    class SafeIpcRenderer { ... }
    
    const registerMessages = () => {
      const safeIpcRenderer = new SafeIpcRenderer(Object.values(MESSAGE_TYPES));
    
      contextBridge.exposeInMainWorld('ELECTRON', {
        sendMessage: safeIpcRenderer.send,
        onReceiveMessage: safeIpcRenderer.on,
        MESSAGE_TYPES,
      });
    };
    
    registerMessages();

main.js


    const registerPreloadImports = require('./src/client/preloadUtils');
    
    // Required if sandbox flag is set to true. Non-electron modules cannot be directly imported in preload script.
    // For more info please check https://www.electronjs.org/docs/api/sandbox-option
    registerPreloadImports();
    
    let mainWindow = new BrowserWindow({
      // Web preferences for mainWindow
      webPreferences: {
        preload: path.join(__dirname, 'src/client/preload.js'),
        contextIsolation: true, // TODO: Remove it once it's enabled by default (from Electron v12)
        disableBlinkFeatures: 'Auxclick',
        sandbox: true,
        // https://www.electronjs.org/docs/api/sandbox-option#status
        enableRemoteModule: false,
      },
    });

preloadUtils.js


    const { ipcMain } = require('electron');
    const MESSAGE_TYPES = require('../utils/messageTypes');
    
    const registerPreloadImports = () => {
      ipcMain.on(MESSAGE_TYPES.GET_MESSAGE_TYPES, (event, message) => {
        event.returnValue = MESSAGE_TYPES;
      });
    };
    
    module.exports = registerPreloadImports;

messageTypes.js


    module.exports = {
      DNS_ONLINE_STATUS: 'dns-online-status',
      APP_ONLINE_STATUS: 'online-status',
      ONLINE_MODEL_SYNC: 'online-model-sync',
      APP_ONLINE: 'app-online',
      INITIAL_DATA_SYNC: 'INITIAL_DATA_SYNC',
      GET_MESSAGE_TYPES: 'GET_MESSAGE_TYPES',
    };

actions.js (renderer)


    const { MESSAGE_TYPES, sendMessage } = window.ELECTRON || {};
    
    if (!MESSAGE_TYPES) return;
    
    const actions = {
      [MESSAGE_TYPES.INITIAL_DATA_SYNC]: (event, initialSync) => {
        console.log(MESSAGE_TYPES.INITIAL_DATA_SYNC, initialSync);
      },
    
      [MESSAGE_TYPES.ONLINE_MODEL_SYNC]: (event, message) => {
        console.log(MESSAGE_TYPES.ONLINE_MODEL_SYNC, message);
      },
    
      [MESSAGE_TYPES.APP_ONLINE]: (event, isOnline) => {
        console.log(MESSAGE_TYPES.APP_ONLINE, isOnline);
      },
    };
    
    const registerActions = () => {
      const { onReceiveMessage } = window.ELECTRON;
    
      Object.keys(actions).forEach((messageType) => {
        onReceiveMessage(messageType, actions[messageType]);
      });
    };
    
    registerActions();

package.json


    {
      "dependencies": {
        "cross-env": "7.0.2",
        "deepmerge": "4.2.2",
        "electron-is-dev": "1.2.0",
        "electron-log": "4.2.2",
        "electron-updater": "4.3.1",
        "sequelize-cli": "6.2.0",
        "sequelize": "6.3.3",
        "sqlite3": "5.0.0",
        "umzug": "2.3.0",
        "uuid": "8.2.0"
      },
      "devDependencies": {
        "concurrently": "5.2.0",
        "electron": "9.1.0",
        "electron-builder": "22.7.0",
        "spectron": "11.1.0",
        "wait-on": "5.1.0",
        "xvfb-maybe": "0.2.1"
      }
    }

zux
  • 23
  • 2