Skip to main content

npm 12 安全迁移指南:安装脚本白名单、Git 依赖与 CI 兼容

August 31, 2026
基于 npm 12 正式安全默认值,讲解安装脚本 allowScripts 审查、Git 与远程依赖治理、严格 CI 验证、原生模块排错及生产回滚。
npm 12 安全迁移指南:安装脚本白名单、Git 依赖与 CI 兼容

更新日期:2026-08-31

npm 12 已经正式发布并进入 latest。这次升级最值得关注的不是界面或输出变化,而是依赖安装的信任模型变了:第三方依赖的安装脚本默认不执行,Git 依赖和远程 tarball 依赖也默认不允许。过去一次普通的 npm ci 可能在没有额外确认的情况下下载二进制、编译原生模块甚至执行依赖包自带的 shell 命令;npm 12 要求项目先说明“谁确实需要这些能力”。

这是一项重要的供应链安全改进,但直接把所有项目统一升级到 npm 12,仍可能得到“安装成功、构建却坏了”的结果。常见原因是 sharpesbuildcanvas、数据库驱动或内部代码生成器依赖安装阶段工作。本文给出一套可审计的迁移方法:先在 npm 11.16 或更新的 npm 11 版本观察,再逐包批准,最后让 npm 12 在 CI 中执行严格门禁。

本文讨论 npm CLI 12 的依赖安装策略,不等同于 Node.js 主版本升级。若项目仍在 Node.js 20,可先阅读 Node.js 20 EOL 后如何迁移到 Node.js 24 LTS,把运行时与包管理器变更拆成可回滚的两个步骤。

先看结论:哪些行为发生了变化

场景npm 11 的常见表现npm 12 默认表现推荐处理
registry 依赖的 preinstallinstallpostinstall通常执行未批准时阻止执行审查后写入 allowScripts
非 registry 依赖的 prepare通常执行受安装脚本策略约束尽量改为已构建的 registry 包
隐式 node-gyp rebuild可能自动执行未批准时不执行只批准确实需要本地编译的包
Git URL 依赖可解析和安装--allow-git=none 默认拒绝改用 registry 固定版本,或显式、限域放行
HTTP(S) tarball 依赖可直接安装--allow-remote=none 默认拒绝迁回 registry,确有必要时精确放行
本地文件或目录依赖可安装默认行为没有随上述两项改变仍需核对打包与工作区边界
npm run build 等显式命令执行项目脚本仍执行指定脚本不要误以为 npm 12 禁掉了所有脚本

这里有两个很重要的区别。

第一,“默认不运行未批准安装脚本”不代表所有脚本都会让安装命令立即失败。团队如果希望 CI 在遇到未审查脚本时直接红灯,还应启用 strict-allow-scripts=true。显式写为 false 的依赖会被跳过;没有任何规则覆盖的依赖,在严格模式下才会让安装失败。

第二,ignore-scripts 不是新的允许列表。它是一把更粗的开关,可能同时跳过项目所需的生命周期步骤;dangerously-allow-all-scripts 则会绕过新策略,只适合极短期诊断,不能作为“先升级再说”的长期配置。

第一步:冻结基线,不在故障现场做调查

开始迁移前,先记录当前能够工作的组合。下面的命令只读取版本和工作区状态:

node --version
npm --version
git status --short
npm config get registry
npm config get ignore-scripts

同时保留当前 package.jsonpackage-lock.json、项目级 .npmrc 和 CI workflow。不要在工作树已有未提交变更时运行会重写 lockfile 的命令。生产项目至少保存以下基线:

  • 一次全新目录中的 npm ci 日志;
  • 单元测试、集成测试和生产构建结果;
  • 需要下载二进制或本地编译的依赖清单;
  • 构建产物的关键文件、启动探针和镜像大小;
  • 当前 Node.js、npm、基础镜像与目标平台架构。

如果应用同时构建 linux/amd64linux/arm64,两种架构都要采样。一个包可能只在某个平台触发可选依赖的安装脚本,本机迁移成功不能替代多架构 CI。

第二步:在 npm 11 中提前发现待审查脚本

npm 官方为迁移提供了观察窗口:使用 npm 11.16 或更新的 npm 11 版本安装依赖,它仍保持旧版兼容行为,但会提示哪些安装脚本尚未审查。先确认实际 CLI,而不是只相信全局环境:

npm --version
npm ci
npm approve-scripts --allow-scripts-pending

最后一条是只读命令,只列出未被 allowScripts 覆盖的包,不修改 package.json。这非常适合作为审查清单,但不能把结果机械地全部批准。

对每一个候选包,至少回答五个问题:

  1. 它为什么需要安装脚本,功能是下载预编译二进制、编译原生模块,还是生成代码?
  2. 它是否为直接依赖?如果是传递依赖,由哪条依赖链引入?
  3. 当前 lockfile 解析到哪个精确版本和下载地址?
  4. 脚本失败后,测试是否真的能捕获问题,还是只有生产启动才会暴露?
  5. 是否能通过升级、换包或预构建产物移除这段安装期执行?

可以用 npm 自带的依赖树查看来源:

npm explain sharp
npm ls sharp --all

随后检查本地安装包中的元数据与脚本。这里查看的是已经由 lockfile 解析的副本,不代表它自动可信:

npm view sharp@0.34.4 dist.integrity dist.tarball scripts --json
node -e "const p=require('./node_modules/sharp/package.json'); console.log(p.version,p.scripts)"

示例中的版本只是演示查询方式;实际审查必须替换为项目 lockfile 中的版本。不要在教程或 CI 中写“永远批准某个包的所有未来版本”。

第三步:逐包写入可审计的 allowScripts

确认某个依赖确实需要运行脚本后,使用官方维护命令,而不是手工猜 package.json 结构:

npm approve-scripts sharp esbuild
git diff -- package.json package-lock.json

默认情况下,npm 会把批准项固定到当前安装版本。这比仅按包名放行更稳妥:依赖升级后会重新进入待审查状态。只有团队明确接受该包所有未来版本、并有其他持续审计机制时,才考虑 --no-allow-scripts-pin

需要拒绝的包应留下明确规则,而不是只在口头上说“不用它”:

npm deny-scripts unwanted-package
git diff -- package.json

千万不要用下面这条命令代替审查:

# 不建议:它会批准所有当前待审查包
npm approve-scripts --all

在大型 monorepo 中还要注意,当前 approve-scripts 命令本身不了解 workspaces。应在拥有策略字段的正确项目根目录执行,并核对变更究竟写进哪个 package.json。如果不同 workspace 有独立 lockfile,应分别验证,不能假设根目录策略自动代表每个发布单元。

第四步:审计 Git 与远程 tarball 依赖

安装脚本只是风险面之一。npm 12 还把 Git 与远程 tarball 依赖默认设置为 none。在升级前,先检查声明文件和 lockfile:

git grep -nE 'git\+|github:|gitlab:|https?://.*\.(tgz|tar\.gz)' -- \
  package.json package-lock.json npm-shrinkwrap.json ':!node_modules'

优先处理顺序应该是:

  1. 用 registry 中的正式、固定版本替换 Git 分支或 commit 依赖;
  2. 如果是内部包,发布到组织私有 registry,并保留访问控制和完整性元数据;
  3. 只有无法迁移时,才根据 npm 12 的配置文档对确切来源进行最小放行;
  4. 为临时放行设置负责人和删除日期,避免永久成为隐形例外。

不要为了恢复旧行为把允许范围直接设为全部。Git 依赖还可能执行 prepare,并受本机 Git 配置、凭据、子模块和构建工具影响;远程 tarball 则绕开了普通 registry 的一些治理能力。它们都应该被当作架构例外,而不是普通包名的另一种写法。

第五步:建立 npm 12 的 CI 迁移矩阵

最安全的方式不是当天替换所有 runner,而是先加一个不阻塞主线的 npm 12 job。它使用与生产一致的 Node.js 主版本、操作系统和构建命令:

name: npm-12-migration

on:
  pull_request:
  workflow_dispatch:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: "24"
          cache: "npm"
      - run: npm install --global npm@12
      - run: npm --version
      - run: npm ci
        env:
          NPM_CONFIG_STRICT_ALLOW_SCRIPTS: "true"
      - run: npm test
      - run: npm run build --if-present

这份迁移 job 故意做了几件事:

  • 打印实际 npm 版本,避免 runner 预装版本变化却没人注意;
  • 使用 npm ci,拒绝 package.json 与 lockfile 不一致;
  • 通过严格模式让遗漏的待审查脚本显式失败;
  • 在安装之后运行真实测试和构建,而不是把“依赖目录生成了”当作迁移完成。

如果组织不允许 CI 动态安装全局 CLI,可以在标准 runner 镜像中预装经批准的 npm 12 版本,并记录镜像 digest。关键不是必须使用哪一种安装方式,而是 CLI 版本可追踪、更新由变更评审驱动。

项目级 npm ci 不应写成 npm ci --allow-scripts=...。npm 官方明确说明,项目安装应由 package.jsonallowScripts 或项目 .npmrc 表达团队政策;命令行的 --allow-scripts 主要面向全局安装、npm execnpx 等没有项目清单可写的场景。

第六步:验证“安装成功”之外的行为

安装脚本被阻止时,有些依赖不会立即报错,而会在第一次调用时才发现二进制不存在。因此验收要覆盖实际能力:

  • 图片服务真正解码并转换一张测试图片;
  • 原生数据库驱动建立临时连接并执行查询;
  • bundler 同时构建开发与生产配置;
  • Prisma、protobuf 或其他生成器确认产物存在且版本匹配;
  • Electron、Playwright 等大体积工具明确区分“安装时下载”和“运行时下载”;
  • 容器在只读根文件系统、非 root 用户下完成启动探针。

对比迁移前后的产物清单也很有帮助:

find dist -type f -print0 | sort -z | xargs -0 sha256sum > dist.sha256

不要要求每个带时间戳或 source map 的文件 hash 完全相同;应先排除非确定性产物,再比较核心 bundle、原生二进制和资源是否缺失。

常见失败与定位顺序

npm ci 成功,但应用启动时报缺少原生模块

先运行 npm approve-scripts --allow-scripts-pending,再查看报错模块是否依赖安装期下载或编译。确认必要后批准精确版本,重新从空目录执行 npm ci,不要只在旧 node_modules 上运行 npm rebuild 掩盖问题。

CI 失败,本机成功

比较 Node/npm 版本、CPU 架构、libc、系统工具和环境变量。可选依赖可能只在 Linux runner 中生效。还要检查本机是否残留旧的 node_modules 或用户级 .npmrc 放行规则。

npm config list --location=project
npm config list --location=user

日志中不要输出 registry token。若必须收集配置,只保留与脚本、Git、remote、registry 域名相关且不含认证信息的条目。

lockfile 太旧,批准项无法固定版本

官方文档说明:如果 registry 依赖在 package-lock.json 中没有 resolved URL,npm 无法核对可信版本,固定版本规则可能一直无法命中。先在独立分支用受控 npm 版本刷新 lockfile,审查 registry URL、integrity 和依赖树变化,再批准脚本。不要把一个巨大的 lockfile 重写和生产 npm 12 切换混在同一次不可回退部署中。

临时恢复了 dangerously-allow-all-scripts

把它当作安全例外处理:限定在单个诊断 job,禁止写进共享 .npmrc,记录启用原因和失效时间。它可以帮助确认“故障确实由脚本策略造成”,但确认以后仍要回到逐包允许或替换依赖。

上线与回滚方案

推荐按四个阶段推进:

  1. 观察期:npm 11.16+ 收集待审查脚本、Git 与远程依赖,不改变生产 runner;
  2. 影子验证:npm 12 CI job 执行严格模式,但暂不阻塞合并;
  3. 合并门禁:影子 job 连续稳定后设为 required check,并固定 runner/npm 版本;
  4. 生产替换:构建镜像切到 npm 12,保持上一版镜像、lockfile 与策略文件可立即恢复。

回滚时恢复的是完整的已验证构建组合,不是仅运行 npm install -g npm@11。至少同时恢复 npm 版本、package-lock.jsonpackage.json 中的策略、基础镜像和已验证制品。数据库迁移或外部 API 变更不应与这次包管理器切换同批上线。

回滚也不代表删除已经完成的安全审查。即使短期退回 npm 11,仍可保留固定版本的允许/拒绝决策,继续清理不必要的安装脚本、Git 依赖和远程 tarball。

可直接用于评审的检查清单

  • 已记录 Node.js、npm、操作系统、架构和 lockfile 基线;
  • 已在 npm 11.16+ 执行只读待审查列表;
  • 每个获准安装脚本都有用途、版本、依赖来源和测试证据;
  • 没有用 approve-scripts --all 代替逐包审查;
  • Git 与远程 tarball 依赖已迁移或形成最小例外;
  • npm 12 CI 使用干净环境和 npm ci
  • 严格模式能阻止新出现的未审查脚本;
  • 原生模块、代码生成、多架构与生产启动均有行为测试;
  • 用户级 .npmrc 不会偷偷覆盖项目策略;
  • 上一版 runner、镜像和制品仍可恢复。

npm 12 的正确迁移结果,不是把一次红色构建重新变绿,而是把过去隐藏在依赖安装过程中的执行权限变成可审查、可版本化、可回滚的项目政策。允许列表越短、固定版本越精确、测试越贴近真实运行,升级带来的安全收益才越可靠。

官方资料与继续阅读