为了在加载远程内容时提供适当的安全级别,声明必须分别启用和禁用BrowserWindow
的contextIsolation
和nodeIntegration
选项。在这种情况下,节点/电子 API 将不适用于主渲染器进程。为了公开特定的功能,窗口的预加载脚本可能会利用Electron的contextBridge
功能,为主渲染器提供对选定节点/电子API的访问。
尽管Electron文档中提供了信息,但总体上缺乏contextBridge
使用的具体示例。一般来说,现有的文档/教程并不专注于在实现Electron应用程序时采用安全实践。
以下是我设法在网上找到的单个contextBridge
使用示例: https://github.com/reZach/secure-electron-template
您是否能够提供可能对实现安全的Electron应用程序(依赖于contextBridge
功能(有用的其他资源/示例?
对contextBridge
最佳实践的见解也受到高度赞赏。
我是模板的作者,让我提供一些您可能会觉得有帮助的背景知识。免责声明:我不是安全研究人员,但这是从多个来源抓取的。
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));
}
};
此代码容易受到客户端打开开发工具并修改window.send
和window.recieve
的函数定义的攻击。然后,客户端可以开始在您的主脚本中 ping 您的 ipcMain.js然后可能会造成一些损害,因为它们可能会绕过预加载 js 中的白名单 ipc 通道。这假设你也在主.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));
}
}
}
);
源
以下是设置all
的步骤
- 在主电子
index.js
文件中,指向BrowserWindow
config 对象中的预加载脚本:
new BrowserWindow({
webPreferences: {
preload: path.resolve(app.getAppPath(), 'preload.js')
}
})
preload.js
需要位于应用文件夹中。我使用 webpack 编译所有内容,所以preload.js
文件不是我的 js 代码所在的地方,而是在 app 目录中,electron-builder
将编译可执行文件。以下是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)
}
})
})
- 所有这些都是节点.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)
就是这样。所有这些代码都是为了让客户端代码能够调用我们在预加载脚本中定义的 nodejs 代码。
我自己也遇到了一些麻烦。我的解决方案是这个预加载.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)
}
},
}
);