Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Electron: How to securely inject global variable into BrowserWindow / BrowserView?

I want to load an external webpage in Electron using BrowserView. It has pretty much the same API as BrowserWindow.

const currentWindow = remote.getCurrentWindow();
const view = new remote.BrowserView({
  webPreferences: {
    // contextIsolation: true,
    partition: 'my-view-partition',
    enableRemoteModule: false,
    nodeIntegration: false,
    preload: `${__dirname}/preload.js`,
    sandbox: true,
  },
});
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');

In my preload.js file, I simply attach a variable to the global object.

process.once('loaded', () => {
  global.baz = 'qux';
});

The app running on localhost:3000 is a React app which references the value like this:

const sharedString = global.baz || 'Not found';

The problem is I have to comment out the setting contextIsolation: true when creating the BrowserView. This exposes a security vulnerability.

Is it possible to (one way - from Electron to the webpage) inject variables into a BrowserView (or BrowserWindow) while still using contextIsolation to make the Electron environment isolated from any changes made to the global environment by the loaded content?

Update: One possible approach could be intercepting the network protocol, but I'm not sure about this 🤔

app.on('ready', () => {
  const { protocol } = session.fromPartition('my-partition')

  protocol.interceptBufferProtocol('https', (req, callback) => {
    if (req.uploadData) {
      // How to handle file uploads?
      callback()
      return
    }

    // This is electron.net, docs: https://electronjs.org/docs/api/net
    net
      .request(req)
      .on('response', (res) => {
        const chunks = []
        res.on('data', (chunk) => {
          chunks.push(Buffer.from(chunk))
        })
        res.on('end', () => {
          const blob = Buffer.concat(chunks)
          const type = res.headers['content-type'] || []
          if (type.includes('text/html') && blob.includes('<head>')) {
            // FIXME?
            const pos = blob.indexOf('<head>')
            // inject contains the Buffer with the injected HTML script
            callback(Buffer.concat([blob.slice(0, pos), inject, blob.slice(pos)]))
          } else {
            callback(blob)
          }
        })
      })
      .on('error', (err) => {
        console.error('error', err)
        callback()
      })
      .end()
  })
})
like image 960
J. Hesters Avatar asked Sep 06 '19 11:09

J. Hesters


2 Answers

After doing some digging, I found a few pull requests for Electron that detail the issue you are having. The first describes a reproducible example very similar to the problem you are describing.

Expected Behavior

https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content A preload script should be able to attach anything to the window or document with contextIsolation: true.

Actual behavior

Anything attached to the window in the preload.js just disappears in the renderer.

It seems the final comment explains that the expected behavior no longer works

It was actually possible until recently, a PR with Isolated Worlds has changed the behavior.

The second has a user suggest what they have found to be their solution:

After many days of research and fiddling with the IPC, I've concluded that the best way is to go the protocol route.

I looked at the docs for BrowserWindow and BrowserView as well as an example that shows the behavior that you desire, but these PRs suggest this is no longer possible (along this route).

Possible Solution

Looking into the documentation, the webContents object you get from view.webContents has the function executeJavaScript, so you could try the following to set the global variable.

...
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');
view.webContents.executeJavaScript("global.baz = 'qux';");
...
like image 90
dodgez Avatar answered Nov 14 '22 23:11

dodgez


Other answers are outdated, use contextBridge be sure to use sendToHost() instead of send()

    // Preload (Isolated World)
    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld(
      'electron',
      {
        doThing: () => ipcRenderer.sendToHost('do-a-thing')
      }
    )
    
    // Renderer (Main World)
    
    window.electron.doThing()
like image 22
quinton.ashley Avatar answered Nov 15 '22 00:11

quinton.ashley