揭示攻击者利用 AI 模型幻觉生成虚假包名进行供应链攻击的新威胁,程序员需警惕 AI 推荐的不存在依赖。
由 Python 的 Seth Larson 在 2025 年创造
打字错误仿冒(Typosquatting)赌的是你会打错。AI 幻觉仿冒(Slopsquatting)赌的是你信任 AI 助手。当模型虚构一个根本不存在的包时,攻击者注册这个名称,等待安装命令被执行。以下是完整的杀伤链、为什么常规防御无法应对它,以及在 npm、Composer 和 pip 中真正有效的防御方法。
想象一下,你的编码助手刚才告诉你运行这个命令:
pip install requests-oauth2-helper
看起来完全正常。你要求了一个干净的方法来在 requests 调用中附加 OAuth2 令牌,这个包名读起来就像会做这件事的东西。命名风格符合 Python 生态习惯,连字符的用法也符合约定,听起来就像其他几十个真实的 PyPI 辅助库。所以你运行它。测试通过。然后你继续。
问题是,当这个模型进行学习时,requests-oauth2-helper 根本不存在。模型编造了它。如果某个攻击者在关注,现在这个名称已经被注册走了。
这就是 slopsquatting:一种供应链攻击,用机器幻觉替代了传统打字错误仿冒中的人为打字失误。这个术语由 Python 软件基金会驻地开发者 Seth Larson 创造,在 2025 年由 Ecosyste.ms 的 Andrew Nesbitt 推广。概念很简单,后果却很严重:你不需要再担心自己手指打错 reqeusts。你的 AI 会满脸自信地交给你一个假包,而你会比相信自己的打字更加相信它。
传统打字错误仿冒需要你犯错。这种不需要。
传统打字错误仿冒由来已久。你在 express 旁注册 expres,在 python-dateutil 旁注册 python-dateutl,然后等待有人手滑。这确实有效,但赔率很低。大多数人能正确拼写 express。攻击者只能钓那万分之一的人。
Slopsquatting 完全消除了对人为失误的依赖。开发者会一字不差地输入这个名称,因为他们从一个比自己更值得信赖的来源复制它:刚为周围代码编写的 AI 助手。犯错的地方已经在上游——模型已经做过了,人类只是忠实地复现这个错误。
关键的转变在这里。在传统打字错误仿冒中,攻击者只能猜测你的手指可能如何出错。在 slopsquatting 中,攻击者根本不需要猜测。他们大规模捕获模型的实际输出,注册其中出现的名称。模型替他们完成了目标选择。
为什么行得通:幻觉频繁出现,而且会重复
如果模型幻觉稀少且随机,slopsquatting 最多只是个趣闻。注册一个假名称,无限期地等待,结果没人会装。事实不是这样,因为这两个假设都不成立。
首先看规模。USENIX Security 2025 的一项研究《We Have a Package for You!》在 16 个大语言模型上生成了 576,000 个 Python 和 JavaScript 代码样本,检查了每个推荐的包。其中 19.7% 的包根本不存在。这不是基准边界的测量误差。这是五分之一。研究记录了 205,474 个不同的幻觉包名称。商业模型表现相对更好(至少平均 5.2%),开源模型更差(至少 21.7%),但都没有人幸免于难。
如果每个幻觉都是独一无二的随机事件,这个规模还能接受——因为攻击者不可能注册二十万个名称,也不知道哪些名称会被实际用到。致命的发现是:幻觉具有可重复性。研究人员选取 500 个产生过假包的提示,每个再运行了 10 遍。43% 的幻觉包每次都出现。58% 在多次运行中都有出现。只有 39% 从未再出现。
想象一下:这些虚构中的将近一半是稳定的模型行为,不是噪声。这彻底改变了经济学。攻击者不需要注册所有名称。他们可以扫描模型输出,筛选出重复出现的名称,只注册那些"粘性"最强的。可重复性就是他们的侦察工具。模型一遍遍告诉他们,未来开发者最可能被给出哪些假包名。
警告 这些名称甚至不必看起来像真实的包。使用 Levenshtein 距离测量,同一研究发现只有 13% 的幻觉名称是真实包名的简单拼写错误。大约 38% 有中等相似度,近半数相似度很低:完全虚构,但在代码上下文中仍然可信。最后这一类会直接绕过打字错误检测,我们稍后会说明原因。
杀伤链,逐步分析
这些都不是理论。这是一个清晰、可重复的步骤序列,每一步都是你在日常工作中见过的。
模型建议一个导入。你申请某个功能。AI 助手编写代码,引入一个适合问题域的依赖。它给出一个名称。这个名称是虚构的,但在语法上合格:遵循生态系统惯例,大小写正确,有"这像是个真实存在的东西"的感觉。
你信任它,因为名称看起来合理。这是至关重要的一步,问题出在心理,不在技术。自信的 AI 输出绕过你的警觉,就像自信的资深工程师的代码审查一样。这个名称用词恰当。没什么能触发"危险"的警报。你在审查逻辑,不会去查证一个四词包名是否真实存在,因为有谁会这么做呢?
安装会执行代码。在 npm 和 pip 中,安装包可以自动触发脚本:postinstall 钩子、setup.py 构建步骤。攻击者的恶意代码不需要你显式导入或调用任何东西。安装完成,代码已经运行了。
凭证被窃取。恶意代码读取构建代理总是有的东西:环境变量、~/.aws/credentials、~/.npmrc 令牌、GITHUB_TOKEN、.env 文件。然后把它们 POST 到攻击者的服务器。从外部看像是包在安装时获取元数据。从内部看,你的 CI 刚把钥匙交给了陌生人。
整个过程出奇地安静。没有漏洞,没有 CVE,没有什么巧妙的内存破坏。开发者信任了一个包名,运行了他们每天都要运行五十次的标准安装命令。这就是整个攻击。
而且人们确实会上当。安全研究员 Bar Lanyado 在 Lasso Security 观察到模型反复幻觉一个叫 huggingface-cli 的 Python 包。他在 PyPI 上用这个精确的名称注册了一个空包做实验。三个月内它获得了超过 15,000 次真实下载,Alibaba 的 GraphTranslator 项目最终在 README 中推荐 pip install huggingface-cli,而真实工具的安装命令是 pip install -U "huggingface_hub[cli]"。Lanyado 的包故意是无害的。真正的 slopsquatter 不会这么善良。
同样的思路,四个生态系统,四种不同的打击范围
简单说"它影响 JavaScript、PHP 和 Go"。准确说是每个生态系统给攻击者的手段不同,了解这些差异能告诉你防御的重点在哪。
npm 给攻击者的最多。生命周期脚本(preinstall、install、postinstall)在 npm install 时自动运行。这是经典攻击向量,也正因此生态系统最近开始行动。pnpm v10 在 2025 年初默认禁用了依赖的生命周期脚本,npm 随后跟进:npm v12 在 2026 年 6 月宣布默认关闭自动脚本执行。除非你升级到这些版本,否则 postinstall 就像指向你 CI 的装满子弹的枪。
package.json(恶意依赖)
{
"name": "requests-oauth2-helper",
"version": "1.0.3",
"scripts": {
"postinstall": "node ./collect.js"
}
}
pip 紧跟其后。源代码分发在安装时运行 setup.py 来计算元数据和构建,这意味着 pip install 时执行任意代码。而轮子(.whl)不会这样在安装时运行代码,这就是为什么 --only-binary 是真正的强化手段,不只是小优化。
# 拒绝源码构建;仅接受预编译轮子
pip install --only-binary :all: requests-oauth2-helper
Composer 按设计是这三者中最安全的。Composer 仅执行根包 composer.json 中定义的脚本。依赖包自己的 scripts 区块会被忽略。所以 slopsquatted 的 Composer 包无法像 npm 包那样给你一个自动的 post-install-cmd。唯一的例外是插件:恶意 Composer 插件可以钩住安装事件,这就是为什么 composer install --no-plugins --no-scripts 存在来处理不受信任的依赖树。恶意代码必须等到你的代码真正调用它,这比"安装时运行"的要求高得多。
Go 根本没有安装脚本,因此攻击链会发生变化。go get 和 go build 不会在你添加软件包时立即运行其中任意的初始化代码。被抢注的 Go 模块中的恶意代码会在程序首次执行该模块时运行:可能通过导入时执行的软件包 init() 函数,也可能在 go test 期间执行。虽会晚些触发,但绝不会永远不触发。
Go 还通过模块代理机制带来了独有的变数。Socket 发现了一个植入后门的 BoltDB 拼写仿冒包 github.com/boltdb-go/bolt。该包于 2021 年 11 月上传并被 Go 模块镜像缓存,随后其 Git 标签被重写,指向了干净的代码。然而,代理仍继续提供已缓存的恶意版本。它在三年多的时间里一直未被发现。原本旨在确保构建可复现的缓存机制,也让被投毒的版本变得经久不衰。
因此,正确的思维模型并不是“一种攻击、三种语言”,而是:npm 和 pip 在安装时触发,Composer 等待插件加载或代码调用,Go 则等待代码执行,之后还会永远记住那个恶意版本。抢注者会选择能为其提供最早、最隐蔽触发方式的生态系统。
令人不安的地方在于,你已经采用的大多数控制措施都是为另一种威胁模型设计的,而恶意包抢注恰好能从它们之间的缝隙中溜过去。
锁文件只能在首次安装之后发挥作用。package-lock.json 或 composer.lock 会固定确切的版本和哈希值,防止依赖项在你不知情的情况下发生变化。对于已经审核并锁定的软件包而言,这非常有用。但恶意包抢注攻击的是一个全新名称的首次安装。锁文件里还没有任何相关记录,因为你从未安装过这个东西。AI 仅仅在 30 秒前才建议使用它。锁文件会忠实记录你首次拉取的恶意版本,并从此将其固定下来。锁文件保护的是连续性,而不是第一次接触。
扫描器寻找的是已知恶意对象,而这种包从未出现过。漏洞扫描器或恶意软件信息源会将软件包与他人已经标记的对象进行匹配。一个昨天刚刚注册、专门针对本季度才开始反复出现的幻觉名称而创建的恶意包,不会有 CVE、安全公告、信誉或历史记录。由于没有任何不良记录,它看起来是干净的。扫描器并没有出故障;它只是在回答另一个问题。“这是已知威胁吗?”并不等于“这是由人类选择的真实软件包吗?”
拼写仿冒检测以编辑距离为依据,而这些名称中有一半根本不像任何现有软件包。防御拼写仿冒的标准方法是计算字符串相似度:需要经过多少次编辑才能把 expres 变成 express?然后标记这些近似名称。但别忘了有关 Levenshtein 距离的研究结果:近一半由幻觉产生的名称与任何真实软件包都高度不同。requests-oauth2-helper 与真实软件包的名称并非只有一个字符之差。它根本不是对现有名称的错误拼写,而是一个听起来恰好很可信的全新发明。没有真实的相邻名称可供计算距离,检测器自然也就没有任何触发依据。
把这三点放在一起,就能看清防御缺口的轮廓。上述每一种防御措施都假定恶意包要么发生了变化(锁文件),要么之前已被标记(扫描器),要么在模仿真实的软件包(拼写仿冒检测)。恶意包抢注不属于其中任何一种。它使用的是一个真正崭新的名称,从未出现过,也不像任何现有名称,并且是由一台语气自信的机器选出来的。这些防御措施并不薄弱,只是瞄准了别处。
好消息是,解决方法并不稀奇。它就是你在对待任何依赖项时都应具备的判断力,只不过需要将其应用于一个你已经开始过度信任的信息来源。你不需要一种全新的产品类别,只需要停止把 AI 推荐的软件包当作可靠引用。
在软件包接触你的机器之前,先验证它是否真实存在并仍在维护。安装模型交给你的任何东西之前,先查一下。它是否存在于注册表中?有多少下载量、多少个版本?最近一次发布是什么时候?发布者是谁?背后是否有一个明显由人类维护的代码仓库?被抢注的软件包通常只创建了几天,下载量是一个可疑的整数,而且没有历史记录。花 30 秒检查一下,就能消灭其中绝大多数威胁。
# npm: does it exist, who owns it, how old is it?
npm view requests-oauth2-helper
# PyPI: check the project page, release history, and maintainers
pip index versions requests-oauth2-helper
如果第一条命令返回 404,模型并不是为你找到了什么不为人知的宝藏,而是凭空捏造了一个。
默认禁用安装脚本。仅这一项控制措施,就能让攻击链中最直接的形式失效。关闭自动执行,只把真正需要的构建步骤作为明确的例外,而不是默认行为:
# Kill lifecycle scripts globally; allow-list the rare package that needs one
npm config set ignore-scripts true
# pnpm v10+ blocks dependency scripts by default.
# Approve only the ones you actually trust:
pnpm approve-builds
# Prefer prebuilt wheels so setup.py never runs at install time
pip install --only-binary :all: <package>
# Untrusted tree? No plugins, no scripts.
composer install --no-plugins --no-scripts
只有大约 2% 的 npm 软件包确实需要使用安装脚本,这正是该生态系统决定默认关闭才是合理做法的原因。你几乎不会损失什么,却能关上恶意载荷赖以进入的大门。
通过带有允许列表的私有注册表代理所有软件包。构建智能体不应能够直接访问公共注册表,并拉取模型随口说出的任何软件包。在前面部署一个代理(Artifactory、Verdaccio、私有 PyPI 或 Composer 的 Satis),只允许拉取已经审核并加入列表的软件包。这样一来,幻觉产生的软件包名称不会在安装之后才失败,而是会在安装之前就失败,因为它从未获得批准。这是一项可扩展的控制措施,因为它把决策依据从“开发者是否注意到了”转变为“它是否在允许列表中”。
为新版本增加成熟期延迟。pnpm v11 推出了 minimumReleaseAge,默认值为 1440 分钟(24 小时)。在软件包版本公开时间达到规定长度之前,它会拒绝安装,从而给社区留出发现明显恶意软件的时间。那些为了抓住刚刚出现的幻觉名称而争分夺秒发布的恶意抢注包,恰好就是冷静期窗口最擅长阻止的对象。
而所有措施背后都有一条根本规则:绝不要仅仅因为 AI 推荐了某个依赖项,就让它进入代码仓库。AI 提供的软件包建议是一种假设,而不是可靠来源。应该像对待一个从未见过的账号发布的 Stack Overflow 回答那样对待它:可能正确,值得核查,但绝不能一眼就相信。整个攻击都取决于第二步中那一瞬间错付的信任。只要验证名称,攻击就无处立足。
令人不安的事实是,模型不会很快停止这种行为。幻觉是这些系统生成文本方式所固有的特性,从模型内部来看,虚假的软件包名称与真实名称完全一样。因此,责任落在了你身上——那个真正执行安装命令的人。助手刚刚给出的名称可能是一个真实的库,也可能是攻击者的软件包披上了真实库的外衣。区分二者的唯一方法,就是在安装之前进行检查;而恶意包抢注所依赖的,正是你不会去检查。
附言:感谢你抽出时间阅读这篇文章!文中表达的观点和看法均为我个人所有。英语不是我的母语,因此我会使用 AI 帮助纠正语法,使文章表达得更加清晰易读。如果仍有任何地方读起来稍显别扭,感谢你的理解!
最初发布于 nazarboyko.com。
喜欢这篇文章吗?欢迎保持联系——我在 LinkedIn 上,很乐意与你交流、分享想法,或者只是打个招呼。👋
部分评论可能仅对已登录的访客可见。登录后即可查看全部评论。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。