Electron中的安全问题
2025-08-15 11:06:42 # 客户端安全

Electron简介

本文参考大佬文章[1],摘取部分个人认为比较重要的内容,产出了这篇文章,算是一个学习笔记吧。

Electron是一个使用Javascript、HTMl和CSS构建桌面应用程序的架构[2]。嵌入ChromiumNode.js到二进制的Electron允许您保持一个Javascript代码代码库并创建在Windows上运行的跨平台应用macOS和Linux一一不需要本地开发经验。

Electron架构

Chromium具备网页渲染能力,Nodejs具备操作系统API的能力。

image-20250812205436209

因此从架构上,Electron分为两个部分:主进程渲染进程

主进程

每个Electron应用都有一个单一的主进程,作为应用程序的入口点。主进程在Node.js环境中运行,这意味着它具有require模块和使用所有Node.js API的能力。

image-20250812205757512

渲染进程

每个Electron应用都会为每个打开的BrowserWindow(与每个网页嵌入)生成一个单独的渲染器进程。恰如起名,渲染器负责渲染网页内容。所以实际上,运行于渲染器进程中的代码是须遵照网页标准的(至少就目前使用的Chromium而言是如此)。

预加载脚本(preload)

主进程可以与操作系统交互,渲染进程只能渲染网页,那么当功能需要操作系统支持的时候,渲染进程如何将需求传递给主进程,主进程又如何将结果传递给渲染进程就是个问题!

Electron设计了一系列的IPC功能,方便主进程和渲染进程间通信,渲染进程的通信通常在preload脚本中发生。

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码。这些脚本虽运行于渲染器的环境中,却因能访问Node.js API而拥有了更多的权限。当然,为了安全考虑,它的API是受限的,主要就是发起IPC请求或监听,将自定义的API和变量等传递给渲染进程使用。

实用进程

Electron 22.0.0中开始引入utility process[3],每个Electron应用程序都可以使用主进程生成多个子进程UtilityProcess API,实用进程(官方翻译叫效率进程)可用于托管,例如:不受信任的服务器,CPU密集型任务或以前容易崩溃的组件托管在主进程或使用Node.jschild_process.fork API生成的进程中。

更多详细参考官网[4],本部分参考流程模型章节[5]

远程调试Electron

很多情况下,Electron应用都是无法打开控制台的,这就给测试增加了麻烦。通过这个远程调试的方法,可以远程打开控制台对应用进行控制台调试,更加方便。

以"夸克"为例:

1
2
3
4
5
6
cd /Applications/Quark.app/Contents/MacOS
# 调试渲染程序
./Quark --remote-debugging-port=9222

# 调试主程序
# ./Quark --inspect=9222

然后Chrome浏览器访问chrome://inspect/#devices

image-20250812212229391

点击inspect就可以了。

image-20250812212309164

涉及安全问题的配置

想要去挖掘到Electron开发的应用漏洞,就要从下面的几个参数配置下手。

nodeIntegration[6]

这个属性是主进程创建渲染进程窗口控制渲染进程是否具备执行NodeJs的能力,如果该属性设置为true,渲染进程一旦出现XSS等漏洞,能够执行Javascript代码,就会导致RCE漏洞。

这个特性在5.0版本开始默认设置为falseElectron中通过IPC通信,可以让渲染进程执行开发者自定义的功能,这种通信更加安全,绝不推荐开启nodeIntegration功能

contextIsolation[7]

上下文隔离,从Electron 12.0.0版本开始默认开启(true)

这个属性是主进程创建渲染进程窗口控制渲染进程是否具备覆盖JavaScript方法的能力,如果该属性设置为false,渲染进程一旦出现XSS等漏洞,能够执行覆盖Javascript代码,配合预加载脚本及主进程定义好的功能,可能会导致RCE漏洞。

Preload预加载脚本[8]

简单粗略来说,Electron = Nodejs + ChromiumNodejs负责系统交互,可以理解为主进程,Chromium负责页面渲染,可以理解为渲染进程。

比较常见的配置是禁止渲染进程执行Node.js代码,同时开启上下文隔离,此时如果渲染进程想要使用操作系统或者硬件的部分功能怎么办?

**通过IPC通信!!!**主进程定义好功能,之后渲染进程只传递数据,这样就可以保证XSS不会轻易导致RCE。但是如果所有渲染进程都可以发IPC也很危险,所以在主进程和渲染进程之间吧,设置了一种叫preload预加载脚本的东西,会在创建窗口的时候指定预加载脚本可以执行部分Node.js代码。

毕竟,preload要代表渲染进程与主进程通信,但是危险的方法都用不了,之后通过一种官方定义的桥将方法暴露给渲染进程,这样渲染进程直接调用方法就好了。

检查预加载preload到底有没有漏洞,主要看两点:

  • 是否存在危险方法:
    • 加载任意Nodejs模块等。
  • 是否存在过度暴露
    • 例如预加载脚本preload将用来IPC通信的对象直接暴露给渲染进程。

预加载脚本文件检查过程中要与主进程关联着看,IPC通信是有暗号的,一一对应。

官方实例:渲染进程到主进程(双向)

我们来看一个官方的案例[9]

其中的模式2:渲染器进程到主进程(双向)

这是一个渲染器进程到主进程通信的案例,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径。

image-20250812221326504

可以看到主进程创建窗口前,先试用ipcMain.handle()创建了监听,并提供了"暗号",即代码中的dialog:openFile,以及对应的处理程序。

预加载脚本preload.js通过contextBridge向渲染进程暴露了方法electronAPI.openFile()方法就可以了,调用后,预加载脚本中ipcRenderer向主进程发送“暗号”dialog:openFile,主进程就明白过来了,交给handleFileOpen函数进行处理。

回到我们刚才提到的两点检查项:

  • 是否存在危险方法?

    如果有一些方法是预加载脚本传递命令字符串,主进程负责执行并返回,那这就属于危险方法了。

  • 是否存在过度暴露?

    如果预加载脚本直接将ipcRenderer暴露给渲染进程,而不是上面的electronAPI.openFile(),那就属于过度暴露。

如果上下文隔离被设置为false,那渲染进程里写的内容就已经没有那么重要了,因为每一个方法都可能被渲染进程覆盖,只能看主进程的实现上有没有危险的内容。

Sandbox[10]

Chromium的一个关键安全特性是,进程可以在沙盒中执行。沙盒通过限制对大多数系统资源的访问来减少恶意代码可能造成的伤害 — 沙盒化的进程只能自由使用CPU周期和内存。为了执行需要额外权限的操作,沙盒处的进程通过专用通信渠道将任务下放给更大权限的进程[11]

Electron 20开始,渲染进程默认启用了沙盒,无需进一步配置。如果你想禁用某个进程的沙盒,请参阅为单个进程禁用沙盒部分。

官方文档中还说了在渲染器中启用 nodeIntegration 时,沙盒也会被禁用

自定义协议[12]

这个部分是经常产生漏洞的地方,检查也就是看其是否合理,有没有目录浏览,目录穿越等,导致的问题主要是本地文件泄露和远程代码执行。

webSecurity[13]

webSecurity是开启同源策略的,默认即开启。

关闭webSecurity可能导致加载其它域的JavaScript脚本。

内容安全策略CSP[14]

内容安全策略(CSP)是应对XSS攻击和数据注入攻击的又一层保护措施。我们建议任何载入到Electron的站点都要开启。

CSP属于是一种白名单机制,能够有效的防止外部JavaScript注入执行等,建议开启,检查方法也比较简单,就看窗口加载的html中是否设置了策略即可

相关的配置类似如下:

1
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">

针对v37.2.6各种配置的实际危害测试

Electron官方开发了Electron Fiddle程序,可以直接选择Electron版本,非常方便,但是需要系统准备对应的NodeJS环境,代码就使用默认的,我们在其中修改配置,进行测试。

测试从一下几个角度进行:

安全配置 测试执行JS点
nodeIntegration 渲染的页面
contextIsolation 预加载脚本(Preload)
sanbox iframe内页面

3个配置选项,2 x 2 x 2 = 8种结果,true开启,false关闭,分布如下:

配置序号 nodeIntegration contextIsolation sandbox
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

测试环境和payload

本次测试的的Electron的版本为:v37.2.6,使用的系统是Macos

测试的Payload:

1
require('child_process').exec('open -a Calculator')

iframe服务器attack.com/1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
</head>

<body>
<div>
<h1>Hello World!</h1>
<script>require('child_process').exec('open -a Calculator')</script>
</div>
</body>

</html>

window.open服务器attack.com/2.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
</head>

<body>
<div>
<h1>Hello World!</h1>
<script>window.open("http://attack.com/3.html")</script>
</div>
</body>

</html>

attack.com/3.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>

<head>
</head>

<body>
<div>
<h1>Hello World!</h1>
<script>require('child_process').exec('open -a Calculator')</script>
</div>
</body>

</html>

为了测试iframe,关闭CSP

默认配置(配置5)

根据官方文档Electron5.0.0.以后,nodeIntegration默认是为false,自12.0.0以后contextIsolation默认为true。从 Electron 20 开始,渲染进程默认启用了沙盒。CSP官方给的案例中是使用了的,这里为了测试iframe,要给CSP去掉。

所以这里的配置为:

1
2
3
nodeIntegration: false
contextIsolation: true
sandbox: true

即这里就是前面表格中所说的配置5的环境。

下面是测试的结果。

预加载脚本preload

image-20250813150300734

运行,发现没弹计算器,失败。

image-20250813150433641

渲染进程

image-20250813150608042

运行,发现没弹计算器,失败。

image-20250813150639369

iframe

image-20250813152310768

运行,发现没弹计算器,失败。

image-20250813152353763

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload
渲染页面
iframe
iframe + window.open

配置1

配置情况:

1
2
3
nodeIntegration: true
contextIsolation: true
sandbox: true

预加载脚本preload

image-20250813165500707

运行,发现没弹计算器,失败。

image-20250813165525210

渲染进程

image-20250813165632111

运行,发现没弹计算器,失败。

image-20250813165725441

iframe

image-20250813165840821

运行,发现没弹计算器,失败。

image-20250813165915917

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload
渲染页面
iframe
iframe + window.open

配置2

配置情况:

1
2
3
nodeIntegration: true
contextIsolation: true
sandbox: false

预加载脚本preload ✔️

image-20250813171051806

运行,成功弹出计算器!!!

image-20250813171130612

渲染脚本

image-20250813171222021

运行,发现没弹计算器,失败。

image-20250813171305546

iframe

image-20250813171408725

运行,发现没弹计算器,失败。

image-20250813171511550

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload 是!!!
渲染页面
iframe
iframe + window.open

配置3

配置情况:

1
2
3
nodeIntegration: true
contextIsolation: false
sandbox: true

预加载脚本preload

image-20250813184951588

运行,发现没弹计算器,失败。

image-20250813185013085

渲染脚本

image-20250813185228740

运行,发现没弹计算器,失败。

image-20250813185305285

iframe

image-20250813185608997

运行,发现没弹计算器,失败。

image-20250813185712866

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload
渲染页面
iframe
iframe + window.open

配置4

配置情况:

1
2
3
nodeIntegration: true
contextIsolation: false
sandbox: false

预加载脚本preload ✔️

image-20250813185936735

运行,成功弹出计算器!!!

image-20250813190002025

渲染脚本 ✔️

image-20250813191715764

运行,成功弹出计算器!!!

image-20250813191807889

iframe

image-20250813191926349

运行,发现没有弹计算器,失败。

image-20250813192034695

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload 是!!!
渲染页面 是!!!
iframe
iframe + window.open

配置6

配置情况:

1
2
3
nodeIntegration: false
contextIsolation: true
sandbox: false

预加载脚本preload ✔️

image-20250813192811068

运行,成功弹出计算器!!!

image-20250813192841807

渲染脚本

image-20250813192920993

失败,未成功弹出计算器。

image-20250813193011605

iframe

image-20250813193805318

失败,未成功弹出计算器。

image-20250813193835196

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload 是!!!
渲染页面
iframe
iframe + window.open

配置7

配置情况:

1
2
3
nodeIntegration: false
contextIsolation: false
sandbox: true

预加载脚本preload

image-20250813202006386

运行,失败,未弹出计算器。

image-20250813202039718

渲染脚本

image-20250813202248359

运行,失败,未成功弹出计算器。

image-20250813202452610

iframe

image-20250813202749553

运行,失败,未弹出计算器。

image-20250813202708369

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload
渲染页面
iframe
iframe + window.open

配置8

配置情况:

1
2
3
nodeIntegration: false
contextIsolation: false
sandbox: false

预加载脚本preload ✔️

image-20250813202955376

运行,成功弹出计算器!!!

image-20250813203040504

渲染脚本

image-20250813204559849

失败,未弹出计算器。

image-20250813204628493

iframe

image-20250813204718885

失败,未弹出计算器

image-20250813204743266

小结

测试执行JS点 是/否可以执行NodeJS
预加载脚本preload 是!!!
渲染页面
iframe
iframe + window.open

总结

了解了前面介绍的一些概念,针对于Electron v37.2.6版本,我对8种的安全配置进行了测试。测试发现只要是sandbox设置为true的均不能成功弹出计算器!不论nodeIntegration和contextIsolation什么配置。还发现了在网页中嵌入<iframe>然后src引入存在payload的页面,发现都没有弹计算器,不论什么配置,且iframe的src引入的页面,再使用window.open打开一个存在payload的页面,依然是不能弹计算器的。

可以得到如下的结论:

安全配置 预加载脚本preload 渲染页面 iframe
nodeIntegration 不影响 true
contextIsolation 不影响 false
sandbox false false
额外条件

这里不谈绕过和覆盖的问题!

由上述表格可以看到:

  • 在预加载脚本preload中,不论nodeIntegrationcontextIsolation的配置是什么,只要sandboxfalse,就可以弹计算器,即就可执行系统命令。
  • 在渲染页面中,nodeIntegration配置为truecontextIsolation配置为falsesandbox配置为false才可以弹计算器,执行系统命令。

有了这些,那我们作为挖掘SRC的,如何去挖掘漏洞呢?

找预加载脚本preload和渲染网页的XSS漏洞!利用XSS漏洞实现RCE!

首先大方向sandbox要配置为false才行,对于预加载脚本preload存在XSS,可以直接执行系统命令。对于渲染页面存在XSS漏洞,需要配置是nodeIntegrationtruecontextIsolationfalse,这样也可直接执行系统命令。

对于渲染页面存在XSS漏洞,sandboxfalse,一定要nodeIntegrationtruecontextIsolationfalse吗?

不一定!前面我也有介绍到了IPC通信!!!主进程和渲染进程之间通信,通过预加载脚本preload进行架桥连接,若preload中存在一些危险的函数,可以执行js,也是可能存在XSS -> RCE的。

补充部分

根据大佬的文章[1:1],经过测试,在Electron 20.0以及以后的版本并不是默认sandbox: true,或者说并不完全等于显式地设置sandbox: true。我在前面的测试例子中都是直接将webPreferences配置中的sandbox直接显示地写出来了,那如果不写这个参数呢?默认的sandbox的配置一定是true吗?答案是不一定!

大佬的结论是:当nodeIntegrationnodeIntegrationInSubFramesnodeIntegrationInWorker被设置为true时,sandbox对于Node.js的保护效果就会失效,当contextIsolation被设置为false时,sandbox对于上下文隔离的保护效果就会失效。

我的测试

我对于大佬的结论进行了测试,发现结论不够严谨。结论中并没有说明nodeIntegrationnodeIntegrationInSubFramesnodeIntegrationInWorker三个都为truesandbox对于Node.js的保护效果就会失效,还是说某几个为true才会失效?

单独开一个nodeIntegrationInSubFrames: true,配合contextIsolation: false,并不会弹出计算器。

image-20250813224836417

单开一个nodeIntegration: true,配合contextIsolation: false,即可在渲染器中实现弹出计算器。这里可以弹计算器,符合了前面说的启用了nodeIntegrationsandbox会被禁用。

image-20250813225057389

经过测试发现,在sandbox不配置,默认的情况下,发现只要nodeIntegration或者nodeIntegrationInWorker有一个为true,preload中就可以执行系统命令。

image-20250814134656484

我还发现,在下面的配置下,渲染器中的iframe是可以弹计算器的,而且要是严格的这个配置。但是发现,window.open是不会弹计算器的。

1
2
3
nodeIntegration: true
nodeIntegrationInSubFrames: true
contextIsolation: false

image-20250814135628734

我的结论

根据上面的多个测试,在Electron v37.2.6版本下的,我的结论如下:

对于显式的写出了sandbox的配置,遵循测试的表格:

安全配置 预加载脚本preload 渲染页面 iframe
nodeIntegration 不影响 true
contextIsolation 不影响 false
sandbox false false
额外条件
  • 在预加载脚本preload中,不论nodeIntegrationcontextIsolation的配置是什么,只要sandboxfalse,就可以弹计算器,即就可执行系统命令。
  • 在渲染页面中,nodeIntegration配置为truecontextIsolation配置为falsesandbox配置为false才可以弹计算器,执行系统命令。

对于未写出sandbox的配置:

对于预加载脚本preload:

sandbox默认是为truenodeIntegrationnodeIntegrationInWorker设置为true时,二者只要有一个true就可sandbox就会被禁用,preload中就可以执行系统命令。

对于渲染页面:

在渲染页面中,nodeIntegrationtruecontextIsolationfalse,必须这样,渲染页面中才能执行系统命令。

iframe中,必须要nodeIntegrationtruenodeIntegrationInSubFramestruecontextIsolationtrue的情况下,渲染器的iframe才能执行系统命令。

安全配置 预加载脚本preload 渲染页面 iframe
nodeIntegration true true true
nodeIntegrationInSubFrames 不影响 不影响 true
nodeIntegrationInWorker true 不影响 不影响
contextIsolation 不影响 false false
额外条件

参考


  1. https://github.com/Just-Hack-For-Fun/Electron-Security ↩︎ ↩︎

  2. https://www.electronjs.org/zh/docs/latest/ ↩︎

  3. https://www.electronjs.org/zh/blog/electron-22-0#utilityprocess-api-36089 ↩︎

  4. https://www.electronjs.org/zh/ ↩︎

  5. https://www.electronjs.org/zh/docs/latest/tutorial/process-model ↩︎

  6. https://www.electronjs.org/zh/docs/latest/tutorial/security#2-不要为远程内容启用-nodejs-集成 ↩︎

  7. https://www.electronjs.org/zh/docs/latest/tutorial/security#3-上下文隔离 ↩︎

  8. https://www.electronjs.org/zh/docs/latest/tutorial/process-model ↩︎

  9. https://www.electronjs.org/zh/docs/latest/tutorial/ipc#模式-2渲染器进程到主进程双向 ↩︎

  10. https://www.electronjs.org/zh/docs/latest/tutorial/sandbox ↩︎

  11. https://www.electronjs.org/zh/docs/latest/tutorial/security#4-启用进程沙盒化 ↩︎

  12. https://www.electronjs.org/zh/docs/latest/api/protocol#protocolregisterschemesasprivilegedcustomschemes ↩︎

  13. https://www.electronjs.org/zh/docs/latest/tutorial/security#6-不要禁用-websecurity ↩︎

  14. https://www.electronjs.org/zh/docs/latest/tutorial/security#7-content-security-policy内容安全策略 ↩︎