本节摘要:沙箱让渲染进程运行在 Chromium 原生的权限隔离体系内——进程降到低权限、剥离文件系统与系统调用访问,Electron 20 起默认开启。本节讲沙箱的底层原理、开启后 preload 脚本失去哪些能力、
sandbox: false的少数正当场景与替代方案,帮助你理解"每一行渲染进程代码都应该默认自己住在牢房里"这条设计哲学。
上下文隔离(2.4 节)限制的是"JavaScript 世界里互相看见什么",沙箱限制的则是"操作系统世界里进程能碰什么"——两道锁一软一硬,互为补充。
Chromium 的沙箱是一套操作系统级机制的组合拳:渲染进程被剥夺直接的文件系统与网络系统调用能力,需要读写时只能通过受审查的代理向 Brokser 侧(在 Electron 里就是主进程)发起请求;进程以低权限令牌运行,即便代码逃逸出 V8,面对的也是一块贫瘠的系统环境。浏览器天天加载不可信网页却大体安全,靠的正是这套体系。Electron 20 把渲染进程默认关进同一座牢房,等于宣布:桌面应用里的页面,应当按"不可信网页"的规格对待。
这与很多开发者的直觉相悖——"我的页面是我自己写的,凭什么当嫌疑人?"答案在 2.4 节已经埋下:页面渲染的内容未必都可信(远程资源、用户输入、第三方脚本库),依赖链上任何一个污染点都可能让"自己的页面"变成攻击者的躯壳。沙箱的作用是把这个假设正式制度化。
先看"失",因为它最常在升级时炸出来。沙箱渲染进程里的 preload 脚本被限制在一个精选 API 集内:contextBridge、ipcRenderer(invoke/send/on 等核心方法)、webFrame、少数工具函数;require 不可用,fs/process 等 Node 设施不可用,任何以 Node 能力为前提的旧 preload 代码都会当场失效。
// 沙箱 preload 里可行的写法:只用桥,不碰 Node const { contextBridge, ipcRenderer } = require('electron'); // 这一个 require 是被允许的 contextBridge.exposeInMainWorld('bridge', { readFile: (name) => ipcRenderer.invoke('file:read', name), // 文件读取请主进程代办 onFileChanged: (cb) => { const listener = (_e, payload) => cb(payload); ipcRenderer.on('file:changed', listener); return () => ipcRenderer.removeListener('file:changed', listener); } });
// 沙箱 preload 里会直接报错的写法 const fs = require('node:fs'); // 不可用:沙箱内无文件系统 const path = require('node:path'); // 不可用:Node 模块体系被裁剪 process.env.ANYTHING; // 不可用:process 为受限替身
"得"则是安全模型的质变:即使渲染进程被完全攻陷——恶意脚本拿到了页面世界的一切、又突破了白名单——它仍然身处操作系统级的隔离牢房,摸不到磁盘、摸不到网络底层、摸不到系统调用。攻击者需要连续突破 V8、沙箱、IPC 白名单三道防线才能碰到真实资产,每多一道,攻击成本指数上升。
| 维度 | 上下文隔离 | 沙箱 |
|---|---|---|
| 作用层面 | JavaScript 上下文 | 操作系统进程权限 |
| 防什么 | 页面脚本摸到 preload 内部与 IPC 本体 | 逃逸代码触碰文件系统与系统调用 |
| 默认状态 | Electron 12 起默认开 | Electron 20 起默认开 |
| 关闭的常见理由 | 几乎没有正当理由 | 少数遗留 Node 依赖 |
诚实地说,沙箱不是零成本开关。三种情况会出现真实的迁移压力。
第一类,历史遗留的 Node 依赖:旧项目的渲染层深度使用 Node 模块(比如在页面里直接做 zip 解压)。正确处置是把这类逻辑整体迁到主进程或 Worker,preload 改走桥接;直接关沙箱属于把技术债转成安全债。
第二类,性能极端敏感的计算:理论上跨进程通信有开销,个别场景想"就在渲染进程里跑 Node"。实践中这几乎总能用 Worker 线程或 WebAssembly 替代,关沙箱仍是下策。
第三类,测试与工具链:某些 E2E 测试框架或调试工具在沙箱下行为受限,开发期局部放开、构建产线保持开启,是可接受的折中。
// 若确需关闭:按窗口粒度、显式声明,而不是全局裸奔 const win = new BrowserWindow({ webPreferences: { sandbox: false, // 明知故犯要写明白,并附 issue 链接说明原因 contextIsolation: true // 隔离无论如何保持开启 } });
一个判断标准送给你:每次想写 sandbox: false 时,先写一段注释说明为什么、计划何时移除。写不出移除计划的关闭,本质上是永久性降级。
背景:某团队的编辑器产品,产品经理要求在页面里直接预览本地工程目录,旧实现靠渲染进程的 Node 递归读盘,升级到新版本后沙箱默认开启,功能全挂。
操作:评审会上没有直接批准关沙箱,而是做了改造——目录遍历移入主进程,新增 fs:list 通道,preload 白名单暴露 listDir 方法,分页返回结构化结果;页面改为增量加载(展开目录时才请求子层)。
结果:功能恢复且比旧版更快(旧实现一次性全量遍历大工程要数秒,增量方案首屏只读两层)。沙箱保持默认开启,安全基线无损。
解读:这个案例的普遍意义在于,"沙箱挡住了我的功能"十有八九是架构问题的信号——渲染进程本来就不该直接干系统级重活。变式:如果遍历逻辑还要被多个窗口共享,那就进一步上收到主进程的单例服务里,窗口只是它的视图。
第二章收束:双人舞的分工、缝合、暗号、安检与牢房全部就位。第三章转入工程视角——用现代流水线把这场舞批量、可复现地搬上舞台。