npm 供应链攻击:20 分钟灌入 630 个恶意版本,你的项目安全吗?

2026-05-20 15 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

5 月 19 日,安全公司 StepSecurity 和 SafeDep 同时发出警告:一场针对 npm 生态的大规模供应链攻击正在进行。攻击者入侵了热门开源项目维护者的账户,在短短 20 分钟内向 317 个 npm 包推送了超过 630 个恶意版本。这场攻击被研究人员命名为 "Mini Shai-Hulud"——它是此前更大规模 Shai-Hulud 攻击的延续,说明攻击者并未收手,而是在迭代手法。

这不是一次孤例,而是 npm 供应链攻击持续升级的信号。对日常依赖 npm 的前端和全栈开发者来说,理解攻击路径并建立防御习惯,比追查每一次具体事件更重要。

攻击是怎么发生的

这次攻击的核心路径并不复杂,但效率极高:

  1. 入侵维护者账户——攻击者获取了热门包的发布权限。入侵方式可能包括凭证泄露、钓鱼或 token 窃取,具体手法尚未完全公开。
  2. 批量发布恶意版本——拿到权限后,攻击者没有逐个手动操作,而是用自动化脚本在 20 分钟内对 317 个包发布了 630 多个版本。每个恶意版本都包含相同的攻击载荷。
  3. 利用现有信任链传播——因为这些包本身是合法的、有用户基础的项目,下游用户在更新或新安装时会自然拉取到恶意版本,触发攻击。

关键点在于:攻击者不需要从零创建伪装包,而是直接篡改已有可信包。用户看到的是熟悉的包名和熟悉的维护者,版本号也符合预期,几乎没有任何肉眼可见的异常。

恶意版本通常做什么

根据此前 Shai-Hulud 系列攻击的分析,这类恶意版本的典型载荷包括:

  • 在安装阶段(postinstall 脚本)执行远程代码,下载并运行第二阶段恶意脚本
  • 窃取环境变量中的凭证(如 AWS_SECRET_ACCESS_KEYNPM_TOKEN
  • 修改本地 .npmrc 或 SSH 配置,为后续攻击铺路
  • 在 CI/CD 环境中横向移动,尝试访问构建服务器上的更多资源

postinstall 是最常见的切入点——它在你执行 npm install 时自动运行,不需要任何用户交互。

检查你的项目是否受影响

第一步是确认你的依赖树中是否包含被篡改的包版本。以下是可直接运行的检查命令:

# 查看当前项目所有依赖的精确版本
npm ls --all --long 2>/dev/null | grep -E "317个受影响包名中的某个"

# 更实用的做法:用 npm audit 检查已知恶意包
npm audit

# 如果你使用 lockfile,检查是否有异常的 resolve URL 或版本跳跃
# 查看最近一周内更新的依赖
npm outdated

由于受影响包列表持续更新,建议直接查阅 StepSecurity 或 SafeDep 发布的 advisory,对照自己的 package-lock.json 逐项排查。

下面是一个更系统的排查脚本,可以扫描 lockfile 中所有包的发布时间,标记出短时间内版本号异常跳跃的条目:

#!/usr/bash
# scan_npm_suspicious.sh
# 扫描 package-lock.json 中疑似被批量篡改的包版本
# 用法: ./scan_npm_suspicious.sh [lockfile路径]

LOCKFILE="${1:-package-lock.json}"

if [ ! -f "$LOCKFILE" ]; then
  echo "找不到 $LOCKFILE,请确认路径"
  exit 1
fi

echo "=== 扫描 $LOCKFILE 中可疑版本跳跃 ==="

# 提取所有包名和版本
packages=$(jq -r '.packages | to_entries[] | select(.key != "") | "\(.key) \(.value.version)"' "$LOCKFILE")

echo "$packages" | while read -r pkg_path version; do
  pkg_name=$(echo "$pkg_path" | sed 's/^node_modules\///')
  # 查询 npm registry 中该包最近5个版本的时间戳
  times=$(npm view "$pkg_name" time --json 2>/dev/null)
  if [ -n "$times" ]; then
    # 检查是否有两个版本在30分钟内连续发布(异常信号)
    quick_publish=$(echo "$times" | jq -r '
      to_entries |
      sort_by(.value) |
      .[-5:] |
      group_by(.value | .[:16]) |
      map(select(length > 2)) |
      length
    ' 2>/dev/null)
    if [ "$quick_publish" -gt 0 ] 2>/dev/null; then
      echo "⚠️  $pkg_name@$version — 近期存在短时间内多次发布"
    fi
  fi
done

echo "=== 扫描完成 ==="

运行前确保你已安装 jqbrew install jqapt install jq)。这个脚本不会修改任何文件,只做读取和标记。

建立日常防御习惯

事后排查是必要的,但更重要的是建立预防机制,让下一次攻击难以渗透到你的项目中。

1. 锁定依赖版本和完整性

# 生成 lockfile 时同时记录完整性哈希
npm install --lockfile-version 3

# 确保 CI 中始终使用 lockfile,不浮动版本
npm ci   # 而不是 npm install

npm ci 严格按 package-lock.json 安装,不会尝试升级或解析新版本。这是 CI/CD 环境中应该唯一使用的安装命令。

2. 禁止 postinstall 脚本自动执行

# 全局禁止 npm 包的 lifecycle 脚本
npm config set ignore-scripts true

# 仅在确认安全后,手动运行必要的脚本
npm rebuild

这会阻断绝大多数供应链攻击的入口。代价是某些依赖 postinstall 的包(如 native addon)需要手动 npm rebuild,但这是一次性的小麻烦,换来的是大幅降低的风险面。

3. 在 CI 中加入自动化审计

# GitHub Actions 示例:每次 PR 自动运行 npm audit
name: Supply Chain Audit
on: [pull_request, push]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm audit --audit-level=high
        # 如果发现 high/critical 漏洞,CI 失败
      # 可选:检查是否有包在最近24h内发布了多个版本
      - run: |
          npm ls --all --json 2>/dev/null | \
          jq -r '.dependencies | to_entries[] | "\(.key) \(.value.version)"' | \
          while read -r name ver; do
            count=$(npm view "$name" versions --json 2>/dev/null | jq 'length')
            recent=$(npm view "$name" time --json 2>/dev/null | \
              jq -r 'to_entries | sort_by(.value) | .[-3:] | map(.value | .[:10]) | unique | length')
            if [ "$recent" -le 1 ]; then
              echo "⚠️ $name@$ver — 近期版本发布密集,请人工复核"
            fi
          done

4. 使用 Sigstore 或 npm provenance 验证包来源

npm 从 v8.11 起支持 provenance(来源证明),在安装时可以验证包是否确实从公开 CI 构建并发布:

# 安装时验证 provenance 签名(需要 npm >= 8.11)
npm install --provenance

# 或在 audit 中检查 provenance
npm audit signatures

如果恶意版本是从被入侵的个人账户直接推送的(而非通过公开 CI),provenance 会显示缺失或异常,成为一道额外屏障。

需要权衡的地方

  • ignore-scripts=true 会阻断攻击,但也让部分包无法正常工作。建议在 CI 中默认开启,本地开发时按需关闭。
  • npm ci 要求 lockfile 必须存在且与 package.json 一致,否则直接报错。这对依赖管理更严格,但需要团队养成提交 lockfile 的习惯。
  • provenance 验证 目前覆盖率还不高,很多老包没有 provenance 签名,npm audit signatures 会报大量"missing",需要过滤噪音。
  • 自动化审计脚本 依赖 npm registry API,在大型项目上可能较慢,建议只在 CI 中运行而非本地每次 install。

下一步行动清单

  1. 立即:运行 npm audit,对照 StepSecurity/SafeDep 的受影响包列表检查你的 package-lock.json
  2. 今天:在 CI 中把 npm install 替换为 npm ci,加入 npm audit --audit-level=high 门禁。
  3. 本周:全局设置 ignore-scripts=true,逐项目确认哪些包需要 npm rebuild,记录在项目 README 中。
  4. 持续:关注 npm provenance 的覆盖率进展,在 CI 中逐步启用 npm audit signatures

供应链攻击不会因为一次警告就停止。Shai-Hulud 系列已经证明攻击者在持续迭代——从更大规模到更快速执行,从创建新包到篡改已有包。防御的关键不是追着每次事件跑,而是把安全检查嵌入日常开发流程,让异常版本在你 npm install 的那一刻就被拦下来。


相关推荐