正文
npm 供应链攻击最麻烦的地方,不是“装错一个包”这么简单。恶意包通常会在安装阶段自动运行,读取开发机和 CI 环境里的 GitHub token、npm token、SSH key、云厂商凭据、.env、Actions secrets,再把这些凭据拿去继续污染其它包或仓库。
所以一旦项目可能装过被投毒的 npm 包,第一反应不要是 rm -rf node_modules。node_modules 只是结果之一。你要先确认恶意版本有没有进入 lockfile、有没有在本机或 CI 里执行过生命周期脚本、有没有接触过密钥、有没有把恶意版本缓存进构建环境。
先看几个真实案例
2026-03-30,StepSecurity 披露 Axios npm 包被投毒。攻击者通过被劫持的维护者账号发布了 axios@1.14.1 和 axios@0.30.4,并加入一个伪装成正常加密库的依赖 plain-crypto-js@4.2.1。这个依赖没有被 Axios 源码引用,作用是通过 postinstall 执行跨平台 RAT dropper。StepSecurity 记录称,恶意版本在 npm 上存活了约 2 到 3 小时,GitHub issue axios/axios#10604 也记录了这两个版本被确认 compromised。
这个案例的危险点是:GitHub 仓库源码看起来可能没问题,npm registry 上的发布物却已经被污染。只看 GitHub tag 或源码 diff,不足以判断你是否安全。
2026-05-19,TechRadar 转述多个安全团队的发现:Mini Shai-Hulud 相关攻击在 1 小时内发布了 639 个恶意版本,覆盖 323 个独立 npm 包,目标包括开发者、开源维护者和 CI/CD 环境。报道提到相关攻击会利用被盗登录凭据和 access token 发布恶意包,窃取 GitHub、云和 CI/CD 相关凭据。
2026-06,Red Hat Cloud Services 命名空间下的 npm 包也被报道遭到污染。TechRadar 报道称,攻击者通过被攻陷的 GitHub 账号向多个 npm 包注入恶意内容,目标包括 GitHub Actions secrets、npm tokens、SSH keys 和 cloud credentials。
这些案例说明,npm 投毒已经不只是“浏览器里偷加密货币钱包地址”这种前端风险。现在攻击者更关心开发者工作台和 CI/CD:那里有发布权限、云权限、仓库权限和更多下游项目。
先确认你有没有装过恶意版本
第一步是查 lockfile,而不是只看 package.json。package.json 里可能只有范围版本,真正装过什么要看锁文件。
PowerShell 里先查已知包名和版本:
Select-String -Path package-lock.json,pnpm-lock.yaml,yarn.lock -Pattern "axios@1.14.1|axios@0.30.4|plain-crypto-js|@tanstack|@mistralai|@redhat-cloud-services" -ErrorAction SilentlyContinue再查最近一次依赖变更:
git diff -- package.json package-lock.json pnpm-lock.yaml yarn.lock重点看这些变化:
| lockfile 变化 | 风险 |
|---|---|
| 新增陌生包 | 可能是被插入的恶意依赖 |
| 原包名没变但 resolved URL 变了 | 可能切到异常 registry 或 tarball |
| integrity 变化 | 说明实际包内容变了 |
新增 postinstall 相关包 | 安装阶段会自动执行代码 |
| 从 registry 包变成 git / tarball 依赖 | 审计和复现更难 |
如果你使用 pnpm,继续看具体路径和版本:
Select-String -Path pnpm-lock.yaml -Pattern "version:|resolution:|integrity:|dependencies:|optionalDependencies:" -Context 0,3如果你使用 npm,确认 CI 是否用的是 npm ci。npm 官方文档说明,npm ci 面向 CI 和部署这类自动化环境,要求已有 package-lock.json 或 npm-shrinkwrap.json;如果 lockfile 和 package.json 不匹配,它会报错,而不是更新 lockfile。这能减少 CI 悄悄装到新版本的概率。
查生命周期脚本和安装时执行
npm 包最危险的入口之一是生命周期脚本:preinstall、install、postinstall、prepare。恶意包常用这些脚本在你安装依赖时自动执行。
先查项目直接依赖里有没有新增脚本:
Get-Content package.json | Select-String -Pattern '"preinstall"|"install"|"postinstall"|"prepare"'再查 lockfile 里是否出现可疑包名后,进入临时目录解包查看发布物:
npm pack <package-name>@<version> --dry-run需要看真实 tarball 内容时,用隔离目录,不要在含密钥的项目里直接运行安装:
mkdir .tmp-npm-check
cd .tmp-npm-check
npm pack <package-name>@<version>
tar -tf .\<package-name>-<version>.tgz看到这些内容要暂停:
| 内容 | 为什么危险 |
|---|---|
postinstall 下载远程脚本 | 安装阶段就能执行任意代码 |
| 混淆 JS / base64 大段字符串 | 可能隐藏下载、执行、外传逻辑 |
curl / wget / PowerShell 下载 | 可能拉第二阶段 payload |
读取 process.env 后发请求 | 可能窃取 CI secret |
读取 ~/.ssh、.npmrc、云配置 | 可能窃取长期凭据 |
自删除、替换 package.json | 可能规避事后检查 |
如果你只是想先让 CI 跑静态检查,可以临时禁用安装脚本:
npm ci --ignore-scripts这不是永久修复。有些合法包需要构建脚本。它的作用是先阻止未知脚本自动运行,让你有机会检查依赖树。
查本机和 CI 是否接触过密钥
只要恶意包可能执行过,就要按“它能读到当前进程权限范围内的内容”处理。开发机和 CI runner 的风险不一样。
| 环境 | 恶意包可能看到什么 |
|---|---|
| 本机终端 | .npmrc、SSH key、Git 凭据、云 CLI 登录、.env |
| GitHub Actions | GITHUB_TOKEN、Actions secrets、OIDC token、部署密钥 |
| Docker build | .npmrc、build args、镜像层、构建缓存 |
| 自托管 runner | 更多本地文件、长期缓存、共享工作目录 |
| 发布流水线 | npm publish token、registry token、GitHub release 权限 |
先查本机敏感文件是否可能被安装脚本读取:
Get-ChildItem -Force $HOME -Recurse -File -ErrorAction SilentlyContinue |
Where-Object {
$_.FullName -match "\\.npmrc|\\.ssh|\\.aws|gcloud|azure|credentials|config"
} |
Select-Object FullName不要把这些文件内容贴到聊天、issue 或日志里。这里只确认路径和暴露面。
再查 CI secrets 相关改动:
rg -n "NPM_TOKEN|NODE_AUTH_TOKEN|GITHUB_TOKEN|AWS_|AZURE_|GCP_|PRIVATE_KEY|DEPLOY_KEY|secrets\\." .github workflows .github/workflows如果恶意包在 CI 中运行过,并且 workflow 里有 npm token、GitHub token、云凭据或部署密钥,直接进入轮换流程。
轮换凭据不要只换 npm token
很多开发者看到 npm 包投毒,只会撤销 npm token。但恶意包不一定只偷 npm token。它能读取当前环境里的所有可访问凭据。
按这个顺序处理:
| 凭据 | 动作 |
|---|---|
| npm token | 立即 revoke,改用短期 granular token 或 Trusted Publishing |
| GitHub PAT | 撤销或轮换,检查最近 API 使用记录 |
GITHUB_TOKEN | 检查 workflow 权限和可写范围 |
| SSH key | 轮换 deploy key、机器 key、个人 key |
| 云厂商 key | 轮换 access key,查 CloudTrail / audit log |
| registry token | 轮换私有 registry、GitHub Packages、Verdaccio token |
| CI secrets | 更新 secrets,清理旧日志和缓存 |
.env 里的生产密码 | 轮换数据库、Redis、JWT、Webhook secret |
如果你不知道某个 token 是否被读过,按读过处理。供应链事件里,“看起来没有被用”不是安全证明。
限制 GitHub Actions 权限
npm 投毒经常盯上 CI,因为 CI 里有自动化权限。GitHub 文档建议在 workflow 或 job 级别配置 GITHUB_TOKEN 权限,只给需要的权限。
给普通测试工作流一个保守默认:
name: test
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
- run: npm ci --ignore-scripts
- run: npm test发布 workflow 再单独提升权限,不要让所有 job 默认拥有写权限:
permissions:
contents: read
id-token: writenpm provenance 文档说明,Trusted Publishing 可以通过 OIDC 生成 provenance attestation,并减少在 CI/CD workflow 里使用长期 access token 的需要。能用 OIDC / Trusted Publishing 的发布流程,优先替代长期 NPM_TOKEN。
发布包时也要注意 provenance 的边界:npm 文档明确说 provenance 不能保证包没有恶意代码,它提供的是可验证的源码和构建说明链接。也就是说,provenance 是判断依据,不是免审通行证。
查 npm 发布来源和 provenance
如果你是包维护者,或者你的项目依赖内部包,继续查发布来源。
先看包元数据:
npm view <package-name>@<version> version dist.tarball dist.integrity repository gitHead _npmUser对比几个版本:
npm view <package-name>@<good-version> _npmUser gitHead dist.integrity
npm view <package-name>@<suspicious-version> _npmUser gitHead dist.integrity异常信号:
| 信号 | 说明 |
|---|---|
| 旧版本由 GitHub Actions / Trusted Publisher 发布,新版本由个人 token 发布 | 可能绕过正常发布链路 |
| npm 上有版本,GitHub 没有 tag / commit | registry 发布物和源码不一致 |
gitHead 缺失或对不上 | 难以追溯源码 |
| 新增陌生 maintainer | 需要确认是否正常授权 |
| 发布间隔异常、版本号异常 | 可能是攻击者抢发 |
能验证签名和 attestations 时,跑:
npm audit signatures签名验证失败不等于一定被投毒,可能是生态支持不完整;但对高风险依赖,它足以触发人工复查。
不要让 lockfile 自动漂移
供应链攻击最喜欢“自动更新”。如果 CI 每次都根据范围版本重新解析依赖,或者 bot 自动合并 lockfile 变化,恶意版本就更容易进来。
更稳的规则:
| 场景 | 做法 |
|---|---|
| CI 安装 | 用 npm ci / pnpm install --frozen-lockfile |
| 依赖升级 PR | 只升级明确包,单独审 lockfile |
| 大版本升级 | 分批升级,不和业务代码混在一个 PR |
| 安全补丁 | 查 advisory、diff、发布来源,再合并 |
| 自动合并 | 禁止自动合并带 lifecycle script 变化的依赖 |
pnpm 项目可以在 CI 里使用:
pnpm install --frozen-lockfileyarn 项目按版本选择:
yarn install --immutable锁文件不是噪音。供应链事件发生时,lockfile 是你判断“到底装过什么”的证据。
发现可疑依赖后的最小处置流程
发现可疑依赖后,按这个顺序处理。
- 暂停部署和发布流水线。
- 保留当前
package.json、lockfile、CI run ID、构建日志和镜像 tag。 - 查本机、CI、容器构建环境是否运行过安装脚本。
- 回退到确认安全的版本,或临时移除该依赖。
- 删除
node_modules后用冻结 lockfile 重装。 - 轮换可能被读取的 token、SSH key、云凭据和数据库密码。
- 清理 CI cache、包管理器 cache、Docker build cache 和远程 runner。
- 检查最近 GitHub Actions、npm publish、registry、云审计日志。
- 补上
permissions、Trusted Publishing、provenance、安装脚本限制。 - 记录影响范围和最终安全版本。
回退示例:
git checkout -- package.json package-lock.json pnpm-lock.yaml yarn.lock
Remove-Item -Recurse -Force node_modules
npm ci --ignore-scripts如果你的项目用 pnpm:
git checkout -- package.json pnpm-lock.yaml
Remove-Item -Recurse -Force node_modules
pnpm install --frozen-lockfile --ignore-scripts--ignore-scripts 只是应急隔离。确认依赖树安全后,再决定是否恢复脚本执行。
发布者要减少长期 token
如果你维护 npm 包,攻击面还包括你的发布账号。
优先顺序:
- 开启 npm 账号 2FA。
- 用 Trusted Publishing / OIDC 替代长期发布 token。
- 如果必须用 token,使用短期、细粒度 token。
- release workflow 只在 tag / release 事件触发。
- 发布 job 单独隔离,不和测试 job 共用过多 secrets。
npm publish前跑npm pack --dry-run看实际发布内容。- 包发布后检查 npm metadata、provenance、GitHub tag 是否一致。
发布前看包里到底有什么:
npm pack --dry-run不要把 .env、测试 token、内部文档、调试脚本、.npmrc、构建缓存发进包。npm 投毒不一定来自外部攻击者;错误发布同样会造成供应链事故。
团队可以固定一张检查表
把这张表放进依赖升级 PR 或安全响应模板里。
npm supply-chain check:
- [ ] package.json and lockfile diff reviewed
- [ ] no unexpected new transitive package
- [ ] no unexpected git / tarball / remote dependency
- [ ] lifecycle scripts reviewed
- [ ] install tested with frozen lockfile
- [ ] CI job runs with least GITHUB_TOKEN permissions
- [ ] npm token is not available to normal test jobs
- [ ] publish uses Trusted Publishing / OIDC where possible
- [ ] npm audit signatures checked for high-risk release
- [ ] CI cache and Docker cache reviewed if suspicious package ran
- [ ] affected tokens and secrets rotated if install script may have runnpm 供应链安全不是“永远不要装依赖”。真正可执行的做法是:锁住版本、看懂 lockfile、限制安装脚本、减少长期 token、让 CI 权限最小化,并在可疑包执行过时按凭据泄露处理。
只要恶意包已经在本机或 CI 里跑过,清理依赖只是第一步。真正的收尾是把它可能接触过的权限都收回来。