← 中文资源

简体中文版本

审计并约束供应链里的 npm 安装脚本

枚举哪些依赖声明了生命周期脚本,用禁脚本模式安装,再为真正需要的例外设白名单。

阅读英文原文

目标

安装脚本是把你机器上的任意代码执行权授予每一个你新增的包。而依赖树里绝大多数包是纯 JavaScript,作为惰性文件安装即可正常工作。枚举真正需要脚本的少数几个、拒绝其余全部,能把供应链攻击面缩小一个数量级:投毒包最多躺在磁盘上,而不是在 CI 安装的那一刻就跑起来。

步骤要点

  1. 从 lockfile 用 jq 枚举带 hasInstallScript 标记的包,通常只占依赖树的百分之几,可以逐个人工评审。
  2. 用 npm ci –ignore-scripts 做全禁脚本安装,观察哪些原生绑定包(如 esbuild)加载失败,这些失败点就是脚本真正在做的事。
  3. 对比两种模式的安装耗时与 node_modules 体积,把禁脚本的收益量化成证据支持策略落地。
  4. 为评审过的例外显式放行:pnpm 用 onlyBuiltDependencies 白名单,npm 则先全禁安装再对白名单 npm rebuild。
  5. 把审计固化为 CI 门禁:解析 lockfile,凡出现白名单之外带安装脚本的包就让构建失败并打印包名,强制人工决策。

排错提示

全禁脚本导致应用崩,从报错栈找到需要的原生包,评审后加入白名单并单独 rebuild;脚本携带者列表频繁变化,就用提交的 lockfile 钉住版本,让门禁只在依赖真正变化时报警;husky 这类自家工具需要 prepare 脚本,把第一方脚本放进白名单或改成文档化的手动步骤,仓内脚本可评审风险更低。

适用边界

禁脚本挡不住 import 之后运行时执行的恶意代码,需配合 SBOM 与 lockfile 审计;白名单在每个原生依赖大版本升级时都要维护更新。

代码示例

# Who executes code at install time?
jq -r '.packages | to_entries[]
  | select(.value | has("hasInstallScript")) | .key' \
  package-lock.json
# node_modules/esbuild      (legit: downloads binary)
# node_modules/sharp        (legit: native bindings)
# node_modules/pkg-x        (why? -> investigate)

# Install everything as inert files, then build the vetted few
npm ci --ignore-scripts
npm rebuild esbuild sharp