5 月 19 日,安全公司 StepSecurity 和 SafeDep 同时发出警告:一场针对 npm 生态的大规模供应链攻击正在进行。攻击者入侵了热门开源项目维护者的账户,在短短 20 分钟内向 317 个 npm 包推送了超过 630 个恶意版本。这场攻击被研究人员命名为 "Mini Shai-Hulud"——它是此前更大规模 Shai-Hulud 攻击的延续,说明攻击者并未收手,而是在迭代手法。
这不是一次孤例,而是 npm 供应链攻击持续升级的信号。对日常依赖 npm 的前端和全栈开发者来说,理解攻击路径并建立防御习惯,比追查每一次具体事件更重要。
攻击是怎么发生的
这次攻击的核心路径并不复杂,但效率极高:
- 入侵维护者账户——攻击者获取了热门包的发布权限。入侵方式可能包括凭证泄露、钓鱼或 token 窃取,具体手法尚未完全公开。
- 批量发布恶意版本——拿到权限后,攻击者没有逐个手动操作,而是用自动化脚本在 20 分钟内对 317 个包发布了 630 多个版本。每个恶意版本都包含相同的攻击载荷。
- 利用现有信任链传播——因为这些包本身是合法的、有用户基础的项目,下游用户在更新或新安装时会自然拉取到恶意版本,触发攻击。
关键点在于:攻击者不需要从零创建伪装包,而是直接篡改已有可信包。用户看到的是熟悉的包名和熟悉的维护者,版本号也符合预期,几乎没有任何肉眼可见的异常。
恶意版本通常做什么
根据此前 Shai-Hulud 系列攻击的分析,这类恶意版本的典型载荷包括:
- 在安装阶段(
postinstall脚本)执行远程代码,下载并运行第二阶段恶意脚本 - 窃取环境变量中的凭证(如
AWS_SECRET_ACCESS_KEY、NPM_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 "=== 扫描完成 ==="
运行前确保你已安装 jq(brew install jq 或 apt 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。
下一步行动清单
- 立即:运行
npm audit,对照 StepSecurity/SafeDep 的受影响包列表检查你的package-lock.json。 - 今天:在 CI 中把
npm install替换为npm ci,加入npm audit --audit-level=high门禁。 - 本周:全局设置
ignore-scripts=true,逐项目确认哪些包需要npm rebuild,记录在项目 README 中。 - 持续:关注 npm provenance 的覆盖率进展,在 CI 中逐步启用
npm audit signatures。
供应链攻击不会因为一次警告就停止。Shai-Hulud 系列已经证明攻击者在持续迭代——从更大规模到更快速执行,从创建新包到篡改已有包。防御的关键不是追着每次事件跑,而是把安全检查嵌入日常开发流程,让异常版本在你 npm install 的那一刻就被拦下来。