包管理器沙箱化现状调查
文章目录
包管理器沙箱化现状调查
Package Manager Sandboxing — nesbitt.io · 14 小时前 · 🔒 安全

包管理器默认把「安装依赖」等同于「以当前用户甚至 root 权限执行第三方代码」,这是这份横跨十余个生态的调查给出的最直接结论:npm 的 postinstall、Cargo 的 build.rs、Python 的 setup.py、gem 的 extconf.rb、apt 与 rpm 的 maintainer script 都会在解析完依赖树之后被执行,于是装一个包在权限模型上等价于运行一个陌生人的程序。文章横向比较了 npm、Yarn、pnpm、Bun、pip 与 uv、Cargo、Go modules、Maven 与 Gradle、apt、dnf、Homebrew、Nix、gem、NuGet 等客户端,按默认行为、可选开关、底层隔离原语三个维度分级。调查显示真正默认开启沙箱的只是少数派:Nix 在 Linux 上默认用 namespace 与只读 bind mount 隔离构建并禁用网络,Gentoo 的 sandbox 特性用 LD_PRELOAD 拦截对构建目录之外的写入,OpenBSD 的 ports 体系借助 pledge 与 unveil。其余绝大多数客户端要么完全没有隔离,要么只提供「关闭脚本」这种粗粒度开关——pnpm 10 把依赖的 postinstall 默认改为不执行并要求在 package.json 的 onlyBuiltDependencies 里显式白名单,npm 提供 --ignore-scripts,pip 与 uv 默认做的是构建隔离而非权限隔离。macOS 上曾经通用的 sandbox-exec 与 Seatbelt profile 已被 Apple 标记为弃用,Linux 侧的替代品是 5.13 进入主线的 Landlock、seccomp-bpf、用户命名空间、bubblewrap、nsjail 与 Firejail。作者的核心判断是:包管理器的信任模型几十年未变,而 2025 年 Shai-Hulud 之类的 postinstall 蠕虫证明这个模型的代价已经无法忽略;短期内现实可行的路线是「默认不执行脚本 + 声明式权限清单 + 内核级最小权限原语」,而不是指望每个客户端各自重新实现一套完整容器。
核心论点
- 默认信任模型的代价在 2025 年被反复验证:npm 生态的 Shai-Hulud 蠕虫借助 postinstall 脚本在被感染包的依赖链上自动传播,并用 TruffleHog 扫描环境变量与文件系统窃取 npm token、GitHub token 与云厂商凭据,再用窃取到的 token 自动发布新版本继续扩散,短期内波及数百个包,同期 Nx 的 s1ngularity 攻击则通过被投毒的构建产物在开发者机器上执行命令;这些事件共同说明,postinstall 这类钩子不需要任何漏洞就能拿到与开发者同等的权限,而 Cargo 的 build.rs、Python 的 setup.py、gem 的 extconf.rb、apt 与 rpm 的 maintainer script 提供了完全等价的入口,其中发行版包管理器的脚本还常常以 root 身份运行。
- 真正默认开启沙箱的实现集中在 Linux 发行版侧:Nix 在 Linux 上默认以 namespace 与只读 bind mount 隔离构建,构建期间禁用网络,并把产物路径写入 store 以保证可复现;Gentoo 的 sandbox 特性用 LD_PRELOAD 劫持文件写入,把构建限制在临时目录内;OpenBSD 的 ports 用 pledge 与 unveil 限制系统调用与路径访问。macOS 曾经的标准答案是 sandbox-exec 与 Seatbelt profile,但 Apple 已将其标记为弃用,导致不少工具转向自建容器或干脆放弃隔离;Linux 侧可用的原语包括 5.13 进入主线的 Landlock LSM、seccomp-bpf、用户命名空间、bubblewrap、nsjail 与 Firejail,它们能组合出「无网络、只写工作目录、系统路径只读」的最小权限环境,但都需要客户端主动接入才生效。
- 客户端层面的开关普遍是粗粒度的:pnpm 10 把依赖的 postinstall 默认改为不执行,开发者必须在 package.json 的 onlyBuiltDependencies 字段里逐个列出允许运行脚本的包名;npm 提供 --ignore-scripts 与 ignore-scripts 配置但默认关闭;Yarn 的 enableScripts 同样是全局开关;pip 与 uv 默认做的是构建隔离,把构建放进临时环境而不是限制其权限,uv 默认拒绝在没有构建后端声明的情况下执行任意 setup.py;Bun 提供部分脚本限制;Deno 的 --allow-read、--allow-write、--allow-net 权限模型最接近沙箱,但它是运行时的默认,而不是包安装的默认;Go modules 与 Maven、Gradle 则把插件与注解处理器放到编译期执行,隔离能力完全取决于宿主环境。
- 沙箱化的难点不在原语而在取舍:构建脚本常常需要联网下载预编译二进制或依赖,禁用网络会直接破坏大量包的安装流程;拦截系统调用带来的性能开销与调试成本、Windows 缺少 Landlock 与 seccomp 等价物导致的跨平台不一致、以及沙箱内缓存失效造成的重复构建,都是客户端作者必须面对的约束。作者的结论是,指望每个包管理器各自实现完整容器并不现实,可行的组合是默认拒绝执行依赖脚本、用声明式清单描述构建所需的网络与文件权限、再交由操作系统的最小权限原语执行,同时把「谁在安装时执行了什么」变成可审计的记录。
💡 为什么值得读: 如果你想弄清 pnpm 10 为什么默认不跑 postinstall、Nix 的构建沙箱到底隔离了什么,以及 2025 年那几场供应链蠕虫留下的真正教训,这份把各生态放进同一张表的横向对比目前很难找到替代品。
🏷️ package managers, sandboxing, security, supply chain
留下一点回声