从突破到壁垒:使用沙盒强化应用
距离 CVE-2023-4863:WebP 堆缓冲区溢出漏洞 公开已过去一周多,导致许多渲染 webp
图像的软件纷纷发布新版本:macOS、iOS、Chrome、Firefox 以及各种 Linux 发行版都收到了更新。此前,Citizen Lab 的调查发现,一个“位于华盛顿特区的公民社会组织”使用的 iPhone,通过 iMessage 中的零点击漏洞遭到攻击。
Electron 也迅速行动,并在当天发布了新版本:如果你的应用程序渲染了任何用户提供的コンテンツ,你应该更新你的 Electron 版本——v27.0.0-beta.2、v26.2.1、v25.8.1、v24.8.3 和 v22.3.24 都包含了修复后的 libwebp
,即负责渲染 webp
图像的库。
现在我们都清楚意识到,“渲染图像”这种看似无害的操作可能是一种危险活动,因此我们想借此机会提醒大家,Electron 自带了一个进程沙箱,可以限制下一次重大攻击的“爆炸半径”——无论它是什么。
沙箱自 Electron v1 起就可用,并在 v20 中默认启用,但我们知道许多应用程序(尤其是那些存在已久的应用程序)的代码中可能存在 sandbox: false
——或者 nodeIntegration: true
,后者在没有明确 sandbox
设置的情况下同样会禁用沙箱。这是可以理解的:如果你与我们同行已久,你可能享受过将 require("child_process")
或 require("fs")
放入运行 HTML/CSS 的相同代码中的便利。
在我们讨论如何迁移到沙箱之前,让我们先讨论一下你为什么需要它。
沙箱为所有渲染进程设置了一个严格的隔离,确保无论内部发生什么,代码都在受限的环境中执行。作为一种概念,它比 Chromium 早得多,并且由所有主流操作系统提供。Electron 和 Chromium 的沙箱建立在这些系统功能之上。即使你从不显示用户生成的コンテンツ,你也应该考虑你的渲染器可能被攻破的可能性:诸如供应链攻击之类复杂的场景,以及像小 bug 这样简单的错误,都可能导致你的渲染器执行你并未完全意图的操作。
沙箱会让这种情况的可怕程度大大降低:内部进程可以自由使用 CPU 周期和内存——仅此而已。进程无法写入磁盘或显示自己的窗口。对于我们的 libwep
漏洞,沙箱确保攻击者无法安装或运行恶意软件。事实上,在最初的 Pegasus 攻击 iPhone 员工的案例中,攻击者特意针对一个非沙箱化的图像进程,以获取手机的访问权限,首先打破了通常被沙箱化的 iMessage 的界限。当像本例中的 CVE 发布时,你仍然需要将你的 Electron 应用升级到安全版本——但在此期间,攻击者造成的损害将大大减小。
将一个标准的 Electron 应用程序从 sandbox: false
迁移到 sandbox: true
是一个艰巨的任务。我知道,因为尽管我亲自撰写了 Electron 安全指南 的初稿,但我自己的一个应用还没有成功迁移到使用它。这个周末我做到了,我建议你也这样做。
你需要解决两个问题
-
如果你在
preload
脚本或实际的WebContents
中使用了 Node.js 代码,你需要将所有 Node.js 交互移到主进程(或者,如果你更高级,可以移到一个工具进程)。考虑到渲染器已经变得如此强大,很可能你大部分代码实际上并不需要重构。查阅我们关于 进程间通信 的文档。对我来说,我迁移了大量代码,并将其包装在
ipcRenderer.invoke()
和ipcMain.handle()
中,但这个过程很简单,而且很快就完成了。在这里要稍微注意你的 API——如果你构建了一个名为executeCodeAsRoot(code)
的 API,沙箱将无法很好地保护你的用户。 -
由于启用沙箱会禁用预加载脚本中的 Node.js 集成,因此你无法再使用
require("../my-script")
。换句话说,你的预加载脚本需要是单个文件。有多种方法可以实现这一点:Webpack、esbuild、parcel 和 rollup 都可以完成这项工作。我使用了 Electron Forge出色的 Webpack 插件,同样流行的
electron-builder
用户可以使用electron-webpack
。
总而言之,整个过程花费了我大约四天时间——这还包括了我为了用各种方式重构我的代码而对如何驾驭 Webpack 的强大功能感到困惑的时间。