2026 年 7 月,npm v12 将正式发布。这次大版本更新没有新功能炫技,而是做了一件很多人喊了多年但一直没落地的事——把安装过程中自动执行脚本的默认行为彻底关掉。所有变更已在 npm 11.16.0+ 以警告形式预演,你现在就能在本地验证影响范围。
三项安全默认值翻转
1. 生命周期脚本默认不再自动执行
preinstall、postinstall、install 等生命周期脚本,从 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.json 或 package-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 全红再慌要好得多。