npm v12 默认锁死脚本执行:供应链攻击的防线终于前移

2026-06-10 16 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

2026 年 7 月,npm v12 将正式发布。这次大版本更新没有新功能炫技,而是做了一件很多人喊了多年但一直没落地的事——把安装过程中自动执行脚本的默认行为彻底关掉。所有变更已在 npm 11.16.0+ 以警告形式预演,你现在就能在本地验证影响范围。

三项安全默认值翻转

1. 生命周期脚本默认不再自动执行

preinstallpostinstallinstall 等生命周期脚本,从 npm v12 起默认不运行。过去任何一个依赖包只要在 package.json 里声明了 "postinstall": "node ./malicious.js"npm install 就会无提示地执行它。这是供应链攻击最常用的入口——postinstall 脚本可以在你毫无察觉的情况下下载远端载荷、修改本地文件、窃取环境变量中的 token。

v12 的默认行为等价于:

npm config set allow-scripts false

只有你显式授权的包才能跑脚本。授权方式见下文实践部分。

2. 外部依赖脚本同样默认关闭

即使你授权了某个包执行脚本,它所依赖的外部包(不在你项目直接依赖树中的包)的脚本仍然默认不执行。这堵住了"我信任 A,但 A 依赖了恶意 B,B 的 postinstall 照跑"的间接攻击路径。

对应配置项:

npm config set allow-external-scripts false

3. 外部依赖本身默认不允许安装

第三项变更更激进:不在你 package.jsonpackage-lock.json 直接声明的外部依赖,默认不会被安装。这针对的是依赖解析中"幽灵依赖"问题——你从未声明的包因为解析链路过长而悄悄进了 node_modules,一旦那个包被劫持,你连它存在都不知道。

npm config set allow-external false

现在就验证:你的项目会断在哪

三项变更已在 npm 11.16.0+ 以弃用警告形式生效。升级到 11.16.0 后执行一次 npm install,终端会打印哪些脚本被跳过、哪些外部依赖被拒绝。这是零风险的预检方式。

下面是一个完整的预检脚本,跑完你就能知道项目在 v12 下能不能正常安装:

# 1. 确认 npm 版本 >= 11.16.0
npm --version

# 2. 临时开启三项严格模式,观察警告(不会修改全局配置)
npm install --allow-scripts=false --allow-external-scripts=false --allow-external=false

# 3. 如果你想看哪些包声明了生命周期脚本,逐项排查
npm ls --all --json | node -e "
  const data = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
  const walk = (obj, path = []) => {
    if (!obj) return;
    for (const [name, meta] of Object.entries(obj)) {
      const scripts = meta?.package?.scripts || {};
      const lifecycle = ['preinstall','install','postinstall','prepare','prepack','postpack'];
      const found = lifecycle.filter(k => scripts[k]);
      if (found.length) console.log(path.concat(name).join(' > '), found);
      if (meta.dependencies) walk(meta.dependencies, path.concat(name));
    }
  };
  walk(data.dependencies);
"

最后一步会列出所有声明了生命周期脚本的包及其脚本名。逐条判断:哪些是你有意依赖的构建工具(比如 esbuild 的 postinstall 下载二进制),哪些你不确定来源。

授权脚本执行的正确姿势

npm v12 提供了精细的包级授权机制,不是一刀切的全开或全关。

在项目的 .npmrc 文件中逐包授权:

# .npmrc — 项目级配置,跟随代码库提交

# 全局默认:不允许脚本执行
allow-scripts=false

# 逐包放行:只信任你审查过的包
allow-scripts.esbuild=true
allow-scripts.@npmcli/git=true

# 外部依赖脚本:默认关闭,逐包放行
allow-external-scripts=false
allow-external-scripts.esbuild=true

# 外部依赖安装:默认关闭,逐包放行
allow-external=false
allow-external.esbuild=true

这样 esbuild 的 postinstall(下载平台对应的预编译二进制)可以正常执行,而其他所有包的脚本一律静默跳过。

如果你确实需要临时全局放行(比如在 CI 中跑完整构建),用命令行覆盖:

# CI 场景:一次性放行所有脚本,但不修改配置文件
npm install --allow-scripts=true --allow-external-scripts=true

更好的做法是在 CI 的 .npmrc 中只放行构建必需的包,避免全开。

升级前的排查清单

步骤 命令 / 操作 目的
升 npm 到 11.16.0+ npm install -g npm@latest-11 获得弃用警告能力
干跑严格模式 npm i --allow-scripts=false --allow-external=false 观察哪些东西被阻断
列出生命周期脚本 上文的 npm ls 管道脚本 找出所有声明 postinstall 等脚本的包
逐包审查 查看每个包的 scripts 字段源码 确认脚本内容是否合理
.npmrc 白名单 allow-scripts.<pkg>=true 只放行审查过的包
锁文件完整性检查 npm ci 代替 npm install 确保锁文件与严格模式兼容
CI 流水线适配 更新 CI 的 .npmrc 或命令行参数 避免构建因脚本跳过而失败

值得注意的边界

  • 构建工具类包(esbuild、sharp、prisma 等)普遍依赖 postinstall 下载平台二进制,v12 下必须显式放行,否则安装后运行时会报 "binary not found"。
  • monorepo 场景中 workspace 内部包的脚本不受 allow-external 限制,但跨 workspace 引用的外部包会被拦截,需要逐个评估。
  • npm ci 在严格模式下行为与 npm install 一致,但更依赖锁文件的准确性——如果你的 package-lock.json 是在旧版 npm 下生成的,建议在 11.16.0 下重新生成一次。
  • 这三项配置都可以在全局 .npmrc 中设置,但强烈建议写在项目级 .npmrc 里并提交到仓库,这样团队所有人共享同一份白名单,不会因个人全局配置不同而出现安装结果不一致。

npm v12 做的不是功能升级,而是安全基线的前移。默认不执行、显式授权才放行——这个原则在容器安全(默认不允许 root)、网络安全(默认拒绝入站)中早已是共识,现在终于落到了包管理器里。提前在 11.16.0 上跑一遍严格模式,比等到 2026 年 7 月突然发现 CI 全红再慌要好得多。


相关推荐