2.5 沙箱(Sandbox)安全模型


2.5 沙箱(Sandbox)安全模型

本节摘要:沙箱让渲染进程运行在 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 方法,分页返回结构化结果;页面改为增量加载(展开目录时才请求子层)。

结果:功能恢复且比旧版更快(旧实现一次性全量遍历大工程要数秒,增量方案首屏只读两层)。沙箱保持默认开启,安全基线无损。

解读:这个案例的普遍意义在于,"沙箱挡住了我的功能"十有八九是架构问题的信号——渲染进程本来就不该直接干系统级重活。变式:如果遍历逻辑还要被多个窗口共享,那就进一步上收到主进程的单例服务里,窗口只是它的视图。

本节要点回顾

  • 双层防御:上下文隔离锁 JS 世界,沙箱锁操作系统世界,一软一硬缺一不可;
  • 默认即嫌疑:页面按不可信内容对待,是制度化而非针对你;
  • preload 裁剪:沙箱下只剩桥类 API,Node 设施一律请主进程代办;
  • 关闭三思:遗留依赖、性能、测试三类诉求都有比关沙箱更好的替代;
  • 评审标准:写不出移除计划的 sandbox: false,等于永久性安全降级。

第二章收束:双人舞的分工、缝合、暗号、安检与牢房全部就位。第三章转入工程视角——用现代流水线把这场舞批量、可复现地搬上舞台。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U