电子“上下文桥”

Que*_*ire 10 javascript security node.js electron

要加载远程内容时提供安全的适当水平,更说明一个BrowserWindowcontextIsolationnodeIntegration选项必须启用和禁用分别。在这种情况下,主渲染器进程将无法使用 Node/Electron API。为了公开特定功能,窗口的预加载脚本可能会利用 Electron 的contextBridge功能,为主渲染器提供对选定 Node/Electron API 的访问权限。

尽管 Electron 文档中提供了信息,contextBridge但总体上缺乏具体的使用示例。一般来说,现有的文档/教程在实现 Electron 应用程序时并不关注采用安全实践。

以下是contextBridge我设法在网上找到的一个使用示例:https : //github.com/reZach/secure-electron-template

您能否提供额外的资源/示例,这些资源/示例可能对实现安全的 Electron 应用程序(依赖于contextBridge功能)有用?

contextBridge最佳实践的洞察力也受到高度赞赏。

dre*_*mLo 11

以下是all设置步骤

  1. 在您的主电子index.js文件中,指向BrowserWindow配置对象中的预加载脚本:

new BrowserWindow({
  webPreferences: {
    preload: path.resolve(app.getAppPath(), 'preload.js')
  }
})
Run Code Online (Sandbox Code Playgroud)

  1. 需要preload.js位于您的应用程序文件夹中。我使用 webpack 来编译所有内容,因此该preload.js文件不是我的 js 代码所在的位置,而是位于electron-builder将编译可执行文件的 app 目录中。以下是其中preload.js包含的内容:

process.once('loaded', () => {
  const { contextBridge, ipcRenderer, shell } = require('electron')
  
  contextBridge.exposeInMainWorld('electron', {
    on (eventName, callback) {
      ipcRenderer.on(eventName, callback)
    },

    async invoke (eventName, ...params) {
      return await ipcRenderer.invoke(eventName, ...params)
    },

    async shellOpenExternal (url) {
      await shell.openExternal(url)
    },

    async shellOpenPath (file) {
      await shell.openPath(file)
    },

    async shellTrashItem (file) {
      await shell.trashItem(file)
    }
  })
})
Run Code Online (Sandbox Code Playgroud)

  1. 所有这些都是 Node.js 代码的一部分。现在让我们看看如何在渲染器代码(即 Chrome 客户端代码)中使用此代码。

window.electron.on('nodeJSEvent', (event, param1, param2) => {
  console.log('nodeJSEvent has been called with params', param1, param2)
})

const foo = await window.electron.invoke('nodeJSEvent', param1, param2)
console.log(foo)

await window.electron.shellOpenExternal(url)

await window.electron.shellOpenPath(file)

await window.electron.shellTrashItem(file)
Run Code Online (Sandbox Code Playgroud)

就是这样。所有这些代码都是为了让客户端代码能够调用我们在预加载脚本中定义的 Nodejs 代码。

  • 你太棒了,我一整天都在寻找这个 (4认同)

Zac*_*Zac 9

我是模板的作者,让我提供一些您可能会觉得有用的背景知识。免责声明:我不是安全研究人员,但这是从多个来源抓取的。

ContextBridge很重要,因为它提供了防止将值传递到基于旧方式的渲染器进程的保护

老办法

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

window.send = function(channel, data){
    // whitelist channels
    let validChannels = ["toMain"];
    if (validChannels.includes(channel)) {
        ipcRenderer.send(channel, data);
    }
};

window.recieve = function(channel, func){
    let validChannels = ["fromMain"];
    if (validChannels.includes(channel)) {
        // Deliberately strip event as it includes `sender` 
        ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
};
Run Code Online (Sandbox Code Playgroud)

此代码很容易受到客户端开放开发工具和修改的功能定义window.sendwindow.recieve。然后客户端可以开始在main.js脚本中ping 您的 ipcMain ,然后可能会造成一些损害,因为它们可以绕过预加载 js 中的白名单 ipc 通道。这假设您也在 main.js 中加入了白名单,但我已经看到很多示例,这些示例没有并且很容易受到这种攻击。

文档

函数值被代理到另一个上下文,所有其他值都被复制和冻结。API 对象中发送的任何数据/原语都变得不可变,并且桥任一侧的更新不会导致另一侧的更新。

换句话说,因为我们使用contextBridge.exposeInMainWorld,我们的渲染器进程无法修改我们公开的函数的定义,从而保护我们免受可能的安全攻击向量。

新方法

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));
            }
        }
    }
);
Run Code Online (Sandbox Code Playgroud)

来源

  • 如果客户端在本地计算机上运行代码,这会带来什么安全风险? (3认同)
  • 如果您的代码加载/使用远程内容,则存在远程内容可能是恶意的并感染/对您的本地计算机造成某些损坏的风险。如果您使用的是所有本地代码,那么它应该是安全的。注意 - 如果您使用 NPM,那么您应该始终谨慎对待您的应用程序已经/正在使用的依赖项。 (3认同)

小智 3

我自己也遇到了一些麻烦。我的解决方案是这个 preload.js 模板

const { ipcRenderer, contextBridge } = require('electron')

const validChannels = ["toMain", "myRenderChannel"];

contextBridge.exposeInMainWorld(
  "api", {
    send: (channel, data) => {
        if (validChannels.includes(channel)) {
            ipcRenderer.send(channel, data);
        }
    },
    on: (channel, callback) => {
      if (validChannels.includes(channel)) {
        // Filtering the event param from ipcRenderer
        const newCallback = (_, data) => callback(data);
        ipcRenderer.on(channel, newCallback);
      }
    },
    once: (channel, callback) => { 
      if (validChannels.includes(channel)) {
        const newCallback = (_, data) => callback(data);
        ipcRenderer.once(channel, newCallback);
      }
    },
    removeListener: (channel, callback) => {
      if (validChannels.includes(channel)) {
        ipcRenderer.removeListener(channel, callback);
      }
    },
    removeAllListeners: (channel) => {
      if (validChannels.includes(channel)) {
        ipcRenderer.removeAllListeners(channel)
      }
    },
  }
);
Run Code Online (Sandbox Code Playgroud)

  • “remove”方法不起作用。因为预加载和渲染器之间的回调不是同一个内存回调。 (2认同)