AI 让你 npm install 的那个包,可能根本不存在:vibe 项目的依赖供应链安全实战
AI 会一本正经地编造不存在的包名,攻击者会抢注这些名字等你来装。USENIX Security 2025 实测:19.7% 的 AI 生成代码引用了不存在的包。这篇指南拆解 slopsquatting 攻击链的四步得手路径,给你安装前 5 分钟的审查动作、lockfile 版本锁定、CI 自动防线和中招后的应急 SOP。

先讲个真事:蠕虫第一次学会了自己繁殖
2025 年 9 月 14 日到 16 日,npm 生态里发生了第一件"蠕虫事件":一个代号 Shai-Hulud 的恶意程序,通过 @ctrl/tinycolor 的 4.1.1 和 4.1.2 版本开始扩散。它的做法很"优雅"——在包里加一行 "postinstall": "node bundle.js",安装时自动执行,打包好的脚本先扫描你机器上的环境变量、~/.npmrc、云厂商凭证,把偷到的东西发到攻击者的 webhook;如果它在你机器上找到了可用的 npm token,就顺手枚举这个账号能发布的所有包,逐个打上相同的恶意补丁版本再发布出去。两天之内,数百个包沦陷,CISA 在 9 月 23 日专门发了警报。
这件事最扎心的地方在于:被投毒的全是"正规包"。发布者账号是真的,签名是对的,来源证明(provenance)也挑不出毛病——签名回答的是"这个包是谁发的",它回答不了"发布者发这个版本的时候是不是自愿的"。当攻击者手里攥着维护者的 token,所有"认脸不认人"的防线都会齐声说"yes"。11 月的第二波甚至蔓延到了 OpenVSX 扩展市场,25,000 多个恶意仓库被创建出来当"死信箱"。
所以这篇指南不谈"依赖要少装"这种正确的废话——站内已经有一篇《AI 装的依赖包,你敢用吗》(slug:vibe-coding-dependency-supply-chain-security)讲过 AI 装包的四种大方习惯和五步落地流程,那篇是"管住 AI 的手";这一篇是"看清攻击者的路":聚焦一条具体的攻击链——slopsquatting(AI 幻觉包名抢注),外加两条名字层面的经典攻击,最后给你安装前 5 分钟的审查动作、CI 自动防线和中招后的应急 SOP。两篇互补,不重复。
/media/content-media/a56f3ccd-38fe-490f-a5d3-6edb6bc228d5/2026/10/5a14d798-94ed-4226-924f-3e880da08db2.png
Slopsquatting 攻击链对照图:左列是攻击者四步得手(AI 幻觉出包名 → 攻击者抢注 → 你 npm install → 构建时恶意脚本执行),右列是每一步对应的拦截动作。图中四步与本节和第 2 节的攻击链描述完全对应,右列动作则分别在第 3、4、5 节展开。第一种攻击面:AI 幻觉包名抢注(slopsquatting)
先给这个词一个出处:slopsquatting 是 Python 软件基金会的开发者 Seth Larson 造的词,专指"攻击者抢注 AI 幻觉出来的包名"这种攻击。而"AI 会幻觉包名"这件事,已经不是都市传说,是 2025 年 USENIX Security 上发表的实测结论。Spracklen 等人的论文《We Have a Package for You!》拿 16 个代码生成模型、57.6 万个生成样本做了统计:19.7% 的样本引用了一个根本不存在的包,总共攒出 205,474 个独一无二的虚构包名。更吓人的是可预测性:把同一个 prompt 重跑 10 次,43% 的幻觉包名每一次都出现,58% 至少出现不止一次。商业模型幻觉率 5.2%,开源模型 21.7%——没有一个模型是干净的。2026 年 4 月云安全联盟(CSA)的后续研究报告把样本量放大到 223 万,19.7% 这个数字纹丝不动。
攻击链是四步,每一步都真实发生过(对应上图左列):
第 1 步:AI 幻觉出包名。你让 agent"装个处理 unused import 的 eslint 插件",它一本正经地写下 npm install unused-imports——这个包在 npm 上根本不存在,真正的包叫 eslint-plugin-unused-imports。AI 不是在骗你,它只是在用训练数据里"听起来像"的模式拼名字。
第 2 步:攻击者抢注。攻击者不用猜你用的什么模型、提的什么需求——论文是公开的,幻觉包名的分布也是公开的,自己跑一遍同样的 prompt 就能收获一份"目标清单"。npm 上的 unused-imports 就这样被抢注,2026 年 2 月时每周还有约 233 次下载;PyPI 上的 huggingface-cli 更夸张,三个月内被下载了 3 万多次——源头是某份公开文档里复制了一条 AI 给的、未经核实的安装命令。
第 3 步:你执行安装。vibe coder 的工作流里这一步几乎没有摩擦:agent 直接装,或者你把 AI 给的命令复制粘贴进终端。npm 的 preinstall/postinstall 脚本在安装时以你的用户权限自动执行,能读文件、能联网——Shai-Hulud 就是靠这一行配置完成初始入侵的。
第 4 步:构建时恶意脚本执行。payload 不一定在你本地爆炸,更常见的是潜伏:偷 CI 里的 token、往你的仓库里提交恶意 workflow(Shai-Hulud 往受害者仓库提交的是 .github/workflows/shai-hulud-workflow.yml),把一次安装变成持续控制。
为什么 vibe coder 是最高危人群?传统开发者装包前至少会瞥一眼 README,而 agent 的工作流是"发现缺包 → 自动安装 → 继续写代码",中间没有任何人看一眼包名是否存在。你的 agent 每分钟能试 10 个包名,攻击者只需要其中一个被抢注过。
第二、三种攻击面:typosquatting 与依赖混淆
slopsquatting 是"AI 替你犯错",下面两种是"名字游戏"的经典款,值得一起讲,因为防御逻辑是同一套:安装前确认这个名字指向的真是你想要的那个包。
typosquatting(拼写陷阱)。攻击者注册与热门包只差一两个字母的名字——react-dom 对 raect-dom,lodash 对 loadsh。你以为只有手抖才会中招?AI 也会"手抖":它生成 import 路径时拼错一个字母,然后顺手把拼错的包也装上。防御上除了安装前查证,还有一招是给团队维护一份"高频包正确拼写表"让 agent 照着抄——别笑,真有用。
依赖混淆(dependency confusion)。2021 年安全研究员 Alex Birsan 的成名作:很多公司的私有包只发布在内部源,但包名是公开的(写在开源代码或文档里)。攻击者在公共源(npm/PyPI)用同名发布一个更高版本号的包,包管理器按"版本号优先"原则就会去公共源拉那个恶意的高版本。Birsan 靠这一招拿到了苹果、微软、PayPal 等大厂的漏洞赏金。vibe 项目的版本:你在用私有 registry 或 GitHub Packages 装内部包,又没配好 scope 映射,某天 AI 帮你 npm install 一个内部包名,公共源上恰好有个同名恶意包等着。防御是配置层面的:.npmrc 里给内部 scope 绑定专属 registry,公共包走公共源,泾渭分明。
三类攻击放一起看,结论很直白:供应链攻击越来越多地发生在"你敲下回车之前"——包名这个最不起眼的环节,才是 2026 年最值得设防的环节。
安装前的 5 分钟审查动作:四查一配
别被"5 分钟"吓到,熟练之后是 1 分钟。核心原则:任何 AI 建议的包,先查证"存在且是它",再谈"好不好用"。注意这和站内旧篇的"让 AI 答辩三个问题"不重复——那是管 AI 的 prompt 约束,这是你(人)在终端里的四个动作加一个配置。
一查:包存在吗?
npm view <包名> version # 不存在会直接报错,这是最便宜的拦截
pip index versions <包名> # Python 同理
slopsquatting 的包有两种状态:还没被抢注(这条命令直接救了你)、已经被抢注(继续往下查)。
二查:发布时间线对得上吗?
npm view <包名> time --json | tail -20
看两个信号:包是不是昨天才发布的(抢注包通常很新);版本历史是不是断层式跳跃(一个三年没动的包突然发了个大版本,要警惕)。Shai-Hulud 的受害者如果看一眼 @ctrl/tinycolor 的发布时间线,会发现 4.1.1 这个版本出现得极其突兀。
三查:维护者与下载量健康吗?打开 npm 页面看三件事:weekly downloads 是不是和它的"名气"匹配(一个号称替代 lodash 的包每周 200 下载,不对劲);maintainers 是不是眼熟的账号;repository 链接能不能点开、是不是真仓库。抢注包最常见的破绽就是"三无":无仓库、无 README、无历史版本。
四查:带不带安装脚本?
npm view <包名> scripts
如果返回里有 preinstall/postinstall,先下载 tarball 把脚本内容读一遍再决定。99% 的纯 JS 工具库不需要安装脚本——"需要编译原生模块"之外的安装脚本,默认当可疑处理。
一配:默认 --ignore-scripts。把这条写进你的 shell profile 和 agent 的系统提示词:
# ~/.npmrc
ignore-scripts=true
副作用是部分包(esbuild、sharp 这类要下载二进制的)装完需要手动 npm rebuild,代价完全可以接受。你要的从来不是"零脚本",而是"脚本执行前我点过头"。
把这五条压成一句话贴在项目 README 里给 agent 看:"新增依赖前必须依次执行存在性、发布时间线、维护者、安装脚本四项检查,默认 ignore-scripts,检查结论写进 PR 描述。"一句话,成本为零,挡掉的是整条 slopsquatting 攻击链的第一步。
lockfile 与版本 pinning:为什么 ^ 是隐患
"lodash": "^4.17.21" 里这个 ^ 是很多 vibe 项目的默认配置,也是供应链攻击最喜欢的形状。^ 允许自动升级 minor 和 patch 版本——而 Shai-Hulud 们最擅长的,就是发布一个"看起来人畜无害"的 patch 版本:@ctrl/tinycolor 从 4.1.0 到 4.1.1,diff 里混进一行 postinstall,没人会去 diff 一个 patch 版本。
三条硬规则:
1. lockfile 必须提交入库,这是底线。package-lock.json(或 pnpm-lock.yaml)记录的是"这次构建实际装了什么"的精确快照。有了它,CI 和队友装出来的依赖树和你本地一字不差;没有它,每次 npm install 都是一次开盲盒。告诉你的 agent 一条铁律:不许删 lockfile,不许用 latest,所有新增依赖必须出现在 lockfile 的 diff 里。
2. 生产安装永远用 npm ci。npm install 会按 ^ 重新解析版本并改写 lockfile,npm ci 则严格按 lockfile 安装,lockfile 和 package.json 对不上就直接报错停下。Dockerfile 里写死 RUN npm ci --omit=dev,devDependencies 永远不上生产——AI 特别爱顺手装 dev 工具,而历史上不少投毒包恰恰藏在 dev 依赖里,因为"反正不上生产"的心态让审查更松。
3. AI 改完 package.json,diff 必查三项。让 agent 养成习惯,每次改完依赖把 diff 贴出来,你只看三行:新增的包名是不是你四查过的;版本号有没有 ^/~/latest(有就改成精确版本);有没有陌生的传递依赖被顺带升级。对已知有漏洞但上游还没修的传递依赖,用 overrides 强行钉死:
{
"overrides": {
"vulnerable-pkg": "1.2.4-patched"
}
}
这三条的本质是把"信任"从"每次安装时重新评估"变成"只在 lockfile 变更时评估一次"。攻击者可以发布无数个恶意 patch 版本,但只要你的构建认 lockfile 不认 ^,这些版本就永远进不了你的机器。
CI 自动防线:audit、osv-scanner 与 Dependabot
人工审查挡的是"你主动装的包",CI 防线挡的是"你没注意到的变化":传递依赖被投毒、已装包的新版本爆出 CVE、AI 半夜替你升级了某个包。先说清楚一个预期:npm audit 只认识已知漏洞库里的 CVE,slopsquatting 这种"全新恶意包"刚出现时不在任何库里。所以 CI 是第二道门,第一道永远是上一节的安装前查证——两道门防的是两种不同的攻击,别指望一道门包打天下。
底线:npm audit 进 CI,卡增量。重点不是"零漏洞"(不现实),而是"新增依赖的增量审计":PR 里多出来的包自动扫一遍,高危直接拦。一个 20 行的 GitHub Action 就够,vibe 项目也值得这 5 分钟:
# .github/workflows/dep-audit.yml
name: dep-audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci --ignore-scripts
- run: npm audit --audit-level=high
进阶:osv-scanner 补全"已知漏洞"的覆盖面。npm audit 只查 npm 官方 advisory 数据库,Google 的 osv-scanner 背后是 OSV 分布式漏洞库,覆盖更多生态和更及时的投毒包记录。同样是 CI 里一行命令,多一双眼睛:
- run: npx osv-scanner --lockfile=package-lock.json --format=json
长期:Dependabot 替你盯着"该升级了"。很多投毒事件的受害者并不是"装了恶意包",而是"守着有已知漏洞的旧版本不升级"。Dependabot 每周自动提 PR,把带安全修复的版本送到你面前,你只需要点 merge。给一人项目的建议节奏是"每周依赖体检 15 分钟":周一早上看一眼 Dependabot 的 PR 列表,合该合的,关该关的,顺手跑一遍 npm audit。日历上设个重复提醒,比任何"安全意识"都管用。
这三件套的成本:配置一次 30 分钟,之后每周 15 分钟。Shai-Hulud 第二波里那些"守着旧 token 不轮换、CI 里常年挂着 npm publish 权限"的项目,缺的从来不是安全知识,是这 30 分钟。
中招了怎么办:应急 SOP
先接受一个现实:防线再全,也有漏网的时候。区别在于有没有 SOP——有 SOP 的是"事故",没 SOP 的是"灾难"。下面这份清单按顺序执行,别跳步:
第 1 步:隔离,别急着删。先停掉 CI 流水线(防止恶意 workflow 继续跑),把可疑的 node_modules 打包留存一份(取证用),再删。删之前用 npm ls <可疑包名> 确认它是怎么进来的——是直接依赖还是第五层传递依赖,这决定第 3 步的范围。
第 2 步:换源重装,钉到干净版本。从 lockfile 的 git 历史里找到上一个已知干净的版本,把版本号 pin 死,删掉 node_modules 和有问题的 lockfile 条目后 npm ci --ignore-scripts 重装。如果是 Shai-Hulud 这类"合法包的恶意版本",去 npm 页面确认官方已下架恶意版本(npm 官方对这类事件响应很快,通常几小时内打标),再升级到修复版。
第 3 步:查仓库有没有被"借尸还魂"。Shai-Hulud 会在受害者仓库提交 .github/workflows/shai-hulud-workflow.yml 这类 workflow,11 月的第二波还会创建名为 SHA1HULUD 的 runner。去 GitHub 的 Actions 页面和仓库设置里搜一遍:最近有没有你没见过的 workflow 文件、有没有陌生的 deploy key、有没有异常的 release。有就删,删完看 git log 确认没有恶意 commit 混进去。
第 4 步:密钥轮换,一份清单。这是最容易被漏掉的一步:npm token、GitHub PAT、云厂商 AK/SK、项目 .env 里的所有密钥、CI secrets——全部轮换一遍,不要心存"应该没偷到"的侥幸。postinstall 脚本跑在安装时,它能看到的和你能看到的一样多。轮换的具体手法(怎么做到不停机轮换、secret 分级)站内《凭据管理》指南有完整做法,这里不复述,直接去看那篇。
第 5 步:上报。去 npm 的 abuse 反馈通道举报恶意包(附上版本号和 payload 行为描述),如果确认是 slopsquatting 抢注,也可以联系 npm 安全团队申请下架。你的举报可能就是别人的第 2 步"二查"里看到的那条"已被下架"记录。
最后说句判断:供应链安全对 vibe coder 从来不是"要不要懂安全"的问题,是"愿不愿意多三道检查"的问题。AI 把写代码的速度提高了十倍,攻击者装包的速度也一样。这场军备竞赛里,赢的永远是那个在
npm install之前多看了一眼的人——而那一眼,本文已经帮你拆成了四个命令。
评论 (0)
相关文章

Agent 越自主,破坏半径越大。给删除、退款、群发、权限变更这类操作加一道“人点头才执行”的闸,是成本最低的保险。这篇指南给你风险×可逆性四象限分级、审批单最小表结构、默认拒绝的超时策略、审计留痕和紧急通道,一个周末就能跑起来。

Simon Willison 发布 Python 小工具 web-edit 0.1a0:让 agent 在远端把 web UI 跑起来,自己经 Tailscale 从手机直接改文件——核心动机是编辑 credentials 文件时,不想让密钥在 LLM 上下文里“过一遍”。工具本身就是他用 Codex Remote 从手机上指挥、GPT-6 Astra Extra High 写出来的。

AI 写代码快,但默认不写安全代码:本地跑通不等于线上安全。这篇实战指南带你补上 vibe 项目的 Web 安全基线——CSP 安全头、CSRF Token、XSS 防护、SQL 参数化、Cookie 三件套、依赖供应链门禁,外加一份可直接复制的上线前 10 项验收清单。