Back

一种应对 npm 供应链攻击的简单防御方法

一种应对 npm 供应链攻击的简单防御方法

每次运行 npm install 时,你都在信任来自数百个你从未阅读过的包的代码。而这种信任,正是攻击者所利用的。在 2025 年 9 月的 Shai-Hulud 蠕虫事件中,恶意的安装时脚本悄无声息地窃取凭据,并大规模重新发布被感染的包——所有这些都在命令提示符返回之前完成。2026 年 3 月的 Axios 入侵事件遵循了相同的模式,其中恶意版本(1.14.10.30.4)引入了一个带有 postinstall 钩子的依赖项,该钩子会执行远程访问载荷。

你不需要复杂的工具就能减少这种风险敞口。一项配置更改即可直接应对最常见的攻击向量。

关键要点

  • npm 生命周期脚本(preinstallinstallpostinstallprepare)会以完整的用户权限自动执行,使其成为供应链攻击中阻力最小的路径。
  • .npmrc 中设置 ignore-scripts=true,只需一行配置即可阻断最常见的攻击向量。
  • 对于像 sharpbcryptsqlite3 这样的原生二进制包,仍可在无脚本安装后使用 npm rebuild 选择性地构建。
  • 在 CI 中检查 package-lock.json 中新增的 "hasInstallScript": true 条目,可在合并前标记出需要审查的依赖项。
  • ignore-scripts=truemin-release-age=7 搭配使用,可以增加一段延迟,有助于避免使用刚被入侵的包版本。

为何生命周期脚本是核心风险

当你安装一个 npm 包时,Node 会自动运行其 package.json 中定义的所有脚本——preinstallinstallpostinstallprepare。这些钩子以你的完整用户权限执行,可访问 ~/.ssh~/.aws/credentials~/.npmrc 中的令牌,以及你 shell 中的每一个环境变量。

没有确认提示。没有沙箱。只有静默执行。

在 Axios 攻击中,一个名为 plain-crypto-js 的恶意传递依赖项利用 postinstall 钩子植入了一个远程访问木马。在 Bitwarden CLI 仿冒攻击中,一个 preinstall 钩子引导加载了一个庞大的混淆载荷,该载荷窃取云凭据,并试图通过受害者的 npm 发布权限进行传播。

生命周期脚本的安全性之所以重要,是因为它代表了整个 JavaScript 依赖安全领域中阻力最小的攻击路径。

核心防御:npm ignore-scripts

向你项目的 .npmrc 中添加一行:

ignore-scripts=true

这会告诉 npm 在安装期间跳过所有生命周期脚本。恶意的 postinstall 钩子根本不会运行。

如果只是临时进行一次安装而不修改 .npmrc:

npm install --ignore-scripts

重要提醒: 这并非完整的解决方案。Bitwarden 攻击通过 package.json 中的 bin 字段注册了一个恶意二进制文件,这意味着当用户调用 bw 命令时,即使 --ignore-scripts 处于激活状态,该载荷仍会执行。没有任何单一的标志能够消除所有风险。

哪些会出问题以及如何处理

某些合法的包需要生命周期脚本来编译原生二进制文件。常见的有 sharpbcryptsqlite3canvaspuppeteer(它会下载 Chromium)。

检查你的项目是否使用了其中任何一个:

npm ls --all | grep -E "(sharp|bcrypt|sqlite3|canvas|puppeteer|node-gyp)"

如果有匹配项,在安装后只重新构建这些特定的包:

npm install --ignore-scripts
npm rebuild sharp --ignore-scripts=false
npm rebuild bcrypt --ignore-scripts=false

这种方法在全局保持脚本禁用的同时,有选择地允许你已明确审查过的包进行编译。

在新安装脚本运行之前检测它们

与其事后被动应对,不如在新依赖项合并之前就标记出带有安装脚本的依赖项。将以下检查添加到你的 CI 流水线中:

# Detect newly added packages with install scripts in PRs
if git diff origin/main -- package-lock.json | \
     grep -E '^\+\s*"hasInstallScript": true' > /dev/null; then
  echo "⚠️ New install script detected—manual review required"
  exit 1
else
  echo "✅ No new install scripts"
fi

在 Axios 攻击中,plain-crypto-js 是一个全新的依赖项,带有 postinstall 钩子。这项检查本可以在 PR 合并之前将其标记出来。

有关 npm 如何在 lockfile 中记录安装脚本的详情,请参阅官方的 package-lock.json 文档

这些防御措施未能涵盖的内容

要诚实面对其局限性:

  • Lockfile 无法防御以下情况:开发者在入侵窗口期间运行了 npm install 并污染了 lockfile,而你没能及时发现。
  • npm audit 只能捕获已知的恶意包——新型攻击不会出现在其数据库中。
  • npm 账户的 2FA 无法保护消费者免受由被入侵的维护者合法发布的包的攻击。
  • --ignore-scripts 无法阻止嵌入在包的运行时 JavaScript 自身中的恶意代码。

分层防御方法会有所帮助。在 .npmrc 中将 ignore-scripts=truemin-release-age=7 搭配使用(需要 npm v11.10.0 或更高版本),以避免安装最近一周内发布的包——这正是大多数攻击处于活跃且未被察觉的时间窗口。该设置的文档可在 npm config 文档的 min-release-age 部分找到。

从这里开始

今天就在你的 .npmrc 中添加:

ignore-scripts=true
min-release-age=7

然后,在每个改动 package-lock.json 的 PR 上添加针对新增 hasInstallScript 条目的 CI 检查。这两项改动直接应对了近期 npm 供应链攻击中所使用的攻击模式——无需新工具、付费服务,也无需对工作流做出重大更改。

结论

你无法阅读所依赖的每个包中的每一行代码,但你可以严格限制这些包在安装期间被允许执行的操作。默认禁用生命周期脚本、只重新构建你信任的原生包,以及在 CI 中标记新增的 hasInstallScript 条目,这些措施合在一起可以化解 npm 生态系统中被利用得最多的攻击向量。所有这些措施都不需要新的供应商或预算——只需在 .npmrc 中添加几行配置和一项 CI 检查。今天就采用它们,下一次类似 Shai-Hulud 的攻击对你的项目造成的影响就会大大减弱。

常见问题

它可能会破坏在安装期间编译原生二进制文件的包,例如 sharp、bcrypt、sqlite3、canvas 和 puppeteer。在禁用脚本运行 npm install 后,使用 npm rebuild 加上包名以及 --ignore-scripts=false 来仅编译你已审查过的可信包。大多数纯 JavaScript 依赖项无需任何更改即可正常工作。

不能。它阻断了最常见的向量——如 postinstall 这样的生命周期钩子——但无法阻止包运行时 JavaScript 中的恶意代码,或通过 bin 字段注册的恶意二进制文件。Bitwarden CLI 仿冒攻击就是以这种方式绕过了 ignore-scripts。请将其与 min-release-age、lockfile 审查和依赖审计相结合,实现分层防御。

min-release-age 设置需要 npm 11.10.0 或更高版本。使用 npm --version 检查你的版本,如有需要,使用 npm install -g npm@latest 进行升级。配置后,使用 npm config get min-release-age 验证该设置是否生效。

将带有 ignore-scripts=true 的 .npmrc 文件提交到仓库,这样 CI 就会自动继承该设置。对于确实需要原生编译的构建,为所需的特定包添加显式的 npm rebuild 命令(带 --ignore-scripts=false)。这样既能保留安全的默认设置,又能明确记录哪些包被允许运行安装代码。

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay