上一篇Electron 安全与你我息息相关文章非常的长,虽然提供了 PDF 版本,但还是导致很多人仅仅是点开看了一下,完读率大概 7.95% 左右,但上一篇真的是我觉得很重要的一篇,对大家了解 Electron 开发的应用程序安全有帮助,与每个人切实相关
但是上一篇文章内容太多,导致很多内容粒度比较粗,可能会给大家造成误解,因此我们打算再写一些文章,一来是将细节补充清楚,二来是再此来呼吁大家注意Electron 安全这件事,如果大家不做出反应,应用程序的开发者是不会有所行动的,这无异于在电脑中埋了一些地雷
我们的公众号已开启了留言功能,大家可以在文章下留言讨论啦~
Electron 的架构以及开发等知识可以参照官网,官网有非常详细的介绍,今天我们要谈论的可能是 Electron 最重要的安全特性之一 —— nodeIntegration
https://www.electronjs.org/zh/docs/latest/ https://www.electronjs.org/zh/docs/latest/tutorial/security#2-%E4%B8%8D%E8%A6%81%E4%B8%BA%E8%BF%9C%E7%A8%8B%E5%86%85%E5%AE%B9%E5%90%AF%E7%94%A8-nodejs-%E9%9B%86%E6%88%90
在 Electron 5 版本以来,这个特性被默认设置为 false
https://releases.electronjs.org/releases/stable?version=5&page=7&limit=2 时间为 2019-04-24 (1815 days ago)
在 Electron 20 版本以来,除非指定 nodeIntegration: true 或 sandbox: false ,不然渲染器会被沙盒化
那这个特性是干什么呢?
官方的解释是:
是否启用
Node integration
官方在安全建议中是这样描述的
加载远程内容时,不论使用的是哪一种渲染器(
BrowserWindow,BrowserView或者webview),最重要的就是绝对不要启用 Node.js 集成。其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 JavaScript 时更难伤害您的用户。
这个描述似乎在说,开启了 nodeIntegration 之后,渲染进程就可以获取到 NodeJS 的能力,这样渲染进程可以直接使用系统相关的方法,进而达到命令执行的效果
官方眼中的渲染器到底具体是什么呢?预加载脚本?渲染页面中的 JS ?是否还包括那些嵌入的页面中的 JS ,他们都可以获取到这种能力吗?
带着这种疑问,我们开始今天的文章
这篇文章在文末也提供了 PDF 版本

我们根据官方重大更改中提到的两个节点 Electron 5.0 和 Electron 20.0 将整个时间线分成四段,取 5 个点,分别测试一下 nodeIntegration 的具体影响
Electron 官方开发了 Electron Fiddle 程序,可以直接选择 Electron 版本,非常方便,但是需要系统准备对应的 NodeJS 环境,代码就使用默认的,我们在其中 "加料",我们主要从以下几个角度测试
https://www.electronjs.org/zh/fiddle
安全配置 | 测试点 |
|---|---|
nodeIntegration | 渲染页面 |
contextIsolation | 预加载脚本 (Preload) |
sanbox | iframe 内页面 |
按照 true 和 false 进行分组
配置序号 | nodeIntegration | contextIsolation | sanbox |
|---|---|---|---|
1 | true | true | true |
2 | true | true | false |
3 | true | false | true |
4 | true | false | false |
5 | false | true | true |
6 | false | true | false |
7 | false | false | true |
8 | false | false | false |
每个 Electron 版本对应的 NodeJS 版本如下
Electron 版本 | NodeJS 版本 |
|---|---|
Electron 3.0 | NodeJS 10.2.0 |
Electron 5.0 | NodeJS 12.0.0 |
Electron 19.0 | NodeJS 16.14.2 |
Electron 20.0 | NodeJS 16.15.0 |
Electron 29.0 | NodeJS 20.9.0 |
使用 nvm 进行多版本管理,用 Deepin Linux 进行测试
https://github.com/nvm-sh/nvm
低版本的 Nodejs 就用 Ubuntu Desktop 18.04 来进行安装,高版本的 Nodejs 用 Deepin Linux 进行安装测试
安装 Fiddle
https://www.electronjs.org/zh/fiddle

安装 nvm
https://github.com/nvm-sh/nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
Deepin
require('child_process').exec('deepin-music')
Payload 是打开 Deepin 音乐程序
Windows 11
require('child_process').exec('calc')
MacOS
require('child_process').exec('open /System/Applications/Calculator.app')
在内网起一个 http 服务器,设置一个恶意的 html
192.168.31.215
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<h1>Hello World!</h1>
<script>require('child_process').exec('deepin-music')</script>
</div>
</body>
</html>

192.168.31.215
在 iframe 中通过 window.open 打开页面进行执行
2.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<h1>Hello World!</h1>
<script>window.open("http://192.168.31.215/3.html")</script>
</div>
</body>
</html>
3.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<h1>Hello World!</h1>
<script>require('child_process').exec('deepin-music')</script>
</div>
</body>
</html>
为了测试 iframe ,关闭 CSP
安装 NodeJs 10.2.0
nvm install 10.2.0


这里会安装失败,可能是版本太老了,所以该用命令行进行安装
sudo npm install -g electron@3.0.0


在 Fiddle 中添加我们自行安装的 Electron 3.0.0





点击 Run 测试是否可以正常显示


能够正常运行
按照官方手册介绍,Electron 3.0 默认安全配置为
nodeIntegration: truecontextIsolation: falsesandbox: false

成功


成功


iframe直接执行失败, window.open 执行成功
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 是 |
nodeIntegration: true
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功


失败


失败
在配置 1 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: false
这回只需要在上一步中不能执行 NodeJS 的环境中测试
肯定可以


失败


失败
在配置 2 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: true
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


失败


失败
在配置 3 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: false
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


成功


在 iframe 中通过 window.open 打开页面进行执行
2.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<h1>Hello World!</h1>
<script>window.open("http://192.168.31.215/3.html")</script>
</div>
</body>
</html>
3.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
<h1>Hello World!</h1>
<script>require('child_process').exec('deepin-music')</script>
</div>
</body>
</html>


成功执行,当然,我是测试过了,在前面的配置中是无法执行的
在配置 4 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 是 |
nodeIntegration: false
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功执行




在配置 5 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: false
contextIsolation: false
sandbox: false
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试






在配置 9 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
剩下就不用测试了了,可以得出结论了
预加载脚本、渲染页面、iframe 测试点在以下情况可以执行 NodeJS
安全配置 | 预加载脚本 | 渲染页面 | iframe |
|---|---|---|---|
nodeIntegration | 不影响 | true | true |
contextIsolation | 不影响 | false | false |
sandbox | 不影响 | false | false |
额外条件 | + window.open |
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 是 |
Electron 5.0 在 Linux 上无法使用 sandbox: true ,所以 sandbox: true 的部分在 Windows 上进行验证
【Windows 部分配置安装】
安装 nvm-windows
https://github.com/coreybutler/nvm-windows
安装 NodeJS 12.0.0
nvm install 12.0.0
nvm use 12.0.0

新建文件夹并创建 NodeJS 项目
https://www.electronjs.org/zh/docs/latest/tutorial/quick-start
npm init

安装 Electron 5.0
npm install --save-dev electron@5.0.0

package.json 中添加启动参数

准备脚本,也就是 Fiddle 中的 main.js、renderer.js、index.html、preload.js

启动项目测试一下
npm run start

可以正常启动
【Linux部分配置安装】
安装 NodeJS 12.0.0
nvm install 12.0.0
nvm use 12.0.0
安装 Electron 5.0
npm install -g electron@5.0.0

与 Electron 3.0 一样,版本较低,需要手动配置


按照官方手册介绍,Electron 5.0 默认安全配置为
nodeIntegration: falsecontextIsolation: truemixed sandbox: truesandbox: false在 5.0 的发布说明中写明了,mixed sandbox 默认启用,但是 mixed sandbox 并不等同于 sandbox,至于 sandbox 具体是什么,在官网也没有搜索到
https://www.electronjs.org/zh/blog/electron-5-0


成功


失败


失败
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功


失败


失败
在配置 1 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: false
这回只需要在上一步中不能执行 NodeJS 的环境中测试
肯定可以


失败


失败
在配置 2 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: true
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


失败


失败
在配置 3 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: false
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


成功


在配置 4 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 是 |
nodeIntegration: false
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功执行


失败


失败
在配置 5 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: false
contextIsolation: false
sandbox: false
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功


失败


失败
在配置 9 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
剩下就不用测试了了,可以得出结论了
预加载脚本、渲染页面、iframe 测试点在以下情况可以执行 NodeJS
安全配置 | 预加载脚本 | 渲染页面 | iframe |
|---|---|---|---|
nodeIntegration | 不影响 | true | true |
contextIsolation | 不影响 | false | false |
sandbox | 不影响 | false | false |
额外条件 | + window.open |
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
安装 NodeJS 16.14.2
nvm install 16.14.2

安装 Electron 19.0
这回直接用 Fiddle 就可以了


按照官方手册介绍,Electron 19.0 默认安全配置
nodeIntegration: falsecontextIsolation: truesandbox: false

成功


失败


失败
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


这里可以看出,预加载脚本就只能执行受限制的 NodeJS 方法了,基本无直接危害
失败


失败


失败
在配置 1 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: false
这回只需要在上一步中不能执行 NodeJS 的环境中测试,也就是全部环境了




失败


失败
在配置 2 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: true
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试


失败


失败


直接执行失败, window.open 执行失败
在配置 3 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: false
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


成功


失败
在配置 4 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: false
contextIsolation: false
sandbox: false
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功


失败


失败
在配置 9 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
剩下就不用测试了了,可以得出结论了
预加载脚本、渲染页面、iframe 测试点在以下情况可以执行 NodeJS
安全配置 | 预加载脚本 | 渲染页面 | iframe |
|---|---|---|---|
nodeIntegration | 不影响 | true | |
contextIsolation | 不影响 | false | |
sandbox | false | false | |
额外条件 |
这里不着急谈绕过和覆盖,那是下一篇文章要探讨的
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
安装 NodeJS 16.15.0
nvm install 16.15.0

安装 Electron 20.0
这回直接用 Fiddle 就可以了

按照官方手册介绍,Electron 20.0 默认安全配置
nodeIntegration: falsecontextIsolation: truesandbox: true在 Deepin Linux 中,Electron 20这个版本开发者工具里面的 console 打不开,一点就闪退,可能是 bug ,所以用 MacOS 进行代替


失败


失败


失败
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


这里可以看出,预加载脚本就只能执行受限制的 NodeJS 方法了,基本无直接危害
失败


失败


失败
在配置 1 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: false
这回只需要在上一步中不能执行 NodeJS 的环境中测试,也就是全部环境了


成功执行


失败


失败
在配置 2 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: true
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试


失败


失败


直接执行失败, window.open 执行失败
在配置 3 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: false
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


成功


失败
在配置 4 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: false
contextIsolation: false
sandbox: false
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功


失败


失败
在配置 9 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: false
contextIsolation: true
sandbox: false


成功




失败
剩下就不用测试了了,可以得出结论了
预加载脚本、渲染页面、iframe 测试点在以下情况可以执行 NodeJS
安全配置 | 预加载脚本 | 渲染页面 | iframe |
|---|---|---|---|
nodeIntegration | 不影响 | true | |
contextIsolation | 不影响 | false | |
sandbox | false | false | |
额外条件 |
这里不着急谈绕过和覆盖,那是下一篇文章要探讨的
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
安装 NodeJS 20.9.0
nvm install 20.9.0

安装 Electron 29.0
这回直接用 Fiddle 就可以了

按照官方手册介绍,Electron 29.0 默认安全配置应该和 20.0 一样
nodeIntegration: falsecontextIsolation: truesandbox: true

失败


失败


失败
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: true
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


失败


失败


失败
在配置 1 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: true
sandbox: false
这回只需要在上一步中不能执行 NodeJS 的环境中测试,也就是全部环境了




失败


失败
在配置 2 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: true
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试


失败


失败


直接执行失败, window.open 执行失败
在配置 3 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: true
contextIsolation: false
sandbox: false
这回只需要在配置 1 中不能执行 NodeJS 的环境中测试
肯定可以


成功


失败
在配置 4 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 是 |
iframe | 否 |
iframe + window.open | 否 |
nodeIntegration: false
contextIsolation: false
sandbox: false
分别尝试在 预加载脚本、渲染进程、iframe 中进行测试


成功


失败


失败
在配置 9 中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 是 |
渲染页面 | 否 |
iframe | 否 |
nodeIntegration: false
contextIsolation: true
sandbox: false


成功


失败,而且因为 bug 还不允许打开 console


失败
剩下就不用测试了了,可以得出结论了
预加载脚本、渲染页面、iframe 测试点在以下情况可以执行 NodeJS
安全配置 | 预加载脚本 | 渲染页面 | iframe |
|---|---|---|---|
nodeIntegration | 不影响 | true | |
contextIsolation | 不影响 | false | |
sandbox | false | false | |
额外条件 |
这里不着急谈绕过和覆盖,那是下一篇文章要探讨的
在默认配置中
测试点 | 是/否可以执行 NodeJS |
|---|---|
预加载脚本 | 否 |
渲染页面 | 否 |
iframe | 否 |
iframe + window.open | 否 |
将以上几个版本的总结贴到一起,如下

相同的配置在不同版本下表现的结果并不完全相同,在 5.0和19.0 之间的重大更改中,并未提及 sandbox 对页面影响的变化,但实际上至少在 19.0 版本开始,预加载脚本想要 NodeJS 能力必须设置 sandbox (这里的 sandbox 是指主进程创建窗口时候的 sandbox),那到底是从哪个版本开始的呢?
所以我们需要再次测试了,但这里就不把过程中展示出来了
从 Electron 6.0.0 开始 sandbox: true 时, Preload 脚本的 NodeJS 环境为受限环境
再此之前即使设置了 sandbox: true预加载脚本还是可以加载并使用require('child_process') 这种模块
经过测试 iframe + window.open 的问题在 Electron 14.0.0 中被修复
从上面的对比结果可以看出, nodeIntegration 的作用确实是主要在于渲染进程是否具备执行 NodeJS 的能力,只有当 nodeIntegration 被设置为 true 时,渲染进程(这里不含 preload) 和 iframe 才有可能获取到执行 NodeJS 的能力,但也同时配合关闭上下文隔离和沙箱
如果采用默认配置,则三个配置项的时间线如下

如果从攻击测试视角查看默认配置项时间线如下

当然,这个是从这篇文章出发的,这篇文章主要讨论 nodeIntegration 这个参数,其他攻击手法及时间线没有放入图中
这里再次呼吁,对于 Electron 程序,我们至少可以按照上一篇文章 Electron安全与你我息息相关 中介绍的普通用户可操作的检查方式对使用的应用程序的安全性进行检查,进而决定使用该程序时的注意事项以及要不要继续使用,当然更好的是向开发者提出建议,采用更安全的开发方式
PDF 版本下载地址
https://pan.baidu.com/s/164fZ7KXzD_jktW_UG3oA2w?pwd=zcj9
我们将 Electron 安全相关的内容建立了一个 Github 项目,地址如下
https://github.com/Just-Hack-For-Fun/Electron-Security