0
0

Agent 为什么会下载一个不存在的仓库:从 HalluSquatting 看资源身份验证

2026-07-13

假设你对一个代码 Agent 说:

帮我克隆并运行某个最近很火的项目。

你只记得项目名,不记得 GitHub 所有者。Agent 根据自己的知识生成了一个看起来合理的地址:

https://github.com/project-name/project-name

这个地址原本并不存在。

但当 Agent 真正访问它时,仓库已经出现了。README 看起来正常,目录结构也像一个开源项目,甚至包含了原项目的大部分功能。Agent 随后按照其中的说明安装依赖、执行脚本,最终把本地文件、凭据或对话上下文发送到了攻击者控制的服务器。

这里最容易产生误解的一点是:Agent 并不是“下载了一个不存在的仓库”。更准确地说,它首先生成了一个原本不存在的资源标识符,而攻击者提前预测并注册了这个标识符,使错误答案在执行时变成了一个真实存在的恶意资源。

这类攻击被称为 HalluSquatting,即 adversarial hallucination squatting,中文可以译为“对抗性幻觉抢注”。

2026 年 7 月发布的一项 HalluSquatting 研究表明,攻击者可以分析模型对热门仓库或 Agent 技能名称的错误输出分布,找出模型反复生成的错误资源标识符,再提前注册这些仓库或技能。此后,攻击者不需要向特定用户发送邮件、共享文档或直接注入恶意提示。Agent 会在正常执行用户任务时,主动把攻击者控制的内容拉进自己的上下文,甚至执行其中的代码。

这说明 Agent 供应链的第一道防线,不是写一段更复杂的系统提示词,而是回答三个更基础的问题:

  1. 当前获取的资源,是不是用户真正想要的资源?

  2. 当前执行的内容,是不是此前审核过的那个版本?

  3. 即使判断错误,执行是否受到隔离、记录和控制?

从 typosquatting 到 slopsquatting,再到 HalluSquatting

这三类攻击相似,但它们利用的错误来源不同。

Typosquatting:利用人的拼写错误

传统 typosquatting 通常注册与合法名称相近的域名或软件包,例如替换一个字母、遗漏一个字符,或者使用视觉上相似的 Unicode 字符。

攻击成立的前提是,人类输入了错误名称,或者没有看出两个名称之间的细微差别。

攻击者预测的是人的失误。

Slopsquatting:利用模型虚构的软件包

Slopsquatting 通常特指软件包场景。大模型生成代码时,可能引用一个在 PyPI、npm 等注册表中并不存在、但名称非常合理的软件包。攻击者收集这些重复出现的虚构名称,并注册同名恶意包。

USENIX Security 2025 的相关研究测试了多种代码模型,发现软件包幻觉并非偶发的随机字符串。一部分虚构包名会在相同或相近提示下反复出现,这种可重复性使抢注攻击具有现实价值。

因此,slopsquatting 利用的不是用户把 requests 拼成了 reqeusts,而是模型主动建议用户安装一个它“认为应该存在”的依赖。

攻击者预测的是模型生成的软件包名称。

HalluSquatting:利用 Agent 生成的任意资源身份

HalluSquatting 将攻击面从软件包扩展到了更一般的外部资源,包括:

  • GitHub 仓库;

  • Agent 技能和插件;

  • MCP Server;

  • 工具市场条目;

  • 下载地址;

  • 容器镜像;

  • 脚本、模型和数据集。

它与 slopsquatting 还有一个关键区别:slopsquatting 通常影响模型生成出来的软件或用户后续构建的程序,而 HalluSquatting 可以直接攻击正在运行的 Agent 本身。

相关研究将这一差异概括为:软件包幻觉攻击主要属于软件供应链攻击,而 HalluSquatting 可以在推理和工具调用阶段把恶意内容拉入 Agent,形成 promptware、工具滥用甚至远程代码执行链。传统 typosquatting 利用人的错误,HalluSquatting 利用的是模型的错误。

模型为什么会生成看似合理、实际不存在的仓库

一个仓库地址看起来像确定性的数据库记录:

owner/repository

但对语言模型而言,它首先是一个需要续写的文本序列。

当模型知道项目名称,却不知道准确的所有者时,它不会天然停止并返回“无法确定”。它可能根据训练数据中的常见模式补全:

project/project
official/project
project-org/project
famous-company/project

这些补全在语言上非常自然,在资源身份上却可能完全错误。

第一,资源标识符具有很强的模式性

GitHub 地址、软件包名称和技能 slug 往往由短单词、连字符和组织名称构成。模型很容易生成符合语法和命名习惯的标识符。

“格式正确”会制造一种危险的可信感:

https://github.com/example-org/example-tool

它看起来比一句含糊的自然语言回答更确定,但这种确定性只是格式带来的,不代表资源身份经过验证。

第二,新资源可能不在模型知识中

HalluSquatting 研究专门比较了较早的知名仓库与新近流行的仓库。在该研究的六个模型和指定实验设置中,2019 年以前的老仓库平均错误率为 0.9%,而一组 2025 年的新仓库平均错误率达到 92.4%。研究者将这种差异主要归因于较新的仓库可能没有进入模型训练数据。该结果不能直接外推到所有模型和所有请求,但它说明“名称流行、身份较新”的资源尤其容易出现解析错误。

第三,显示名称不等于规范标识符

用户记住的通常是产品名,而不是平台内部的完整身份。

例如,一个技能可能显示为:

Financial Search Engine

真实 slug 却可能是:

financial-ai-analyst/mx-finance-search

模型更可能根据显示名称生成:

financial-search-engine

这个结果语义上更自然,却不是真实资源。攻击者只需要把这个更自然的名称提前注册下来。

第四,Agent 把知识错误变成了执行动作

普通聊天模型给出错误地址,最多导致用户打开一个错误页面。

Agent 则可能自动完成整个动作链:

生成地址
→ 搜索或访问
→ 克隆仓库
→ 阅读 README
→ 安装依赖
→ 运行初始化脚本
→ 调用本地工具

模型的事实错误因此不再只是“回答不准确”,而是成为供应链入口。

风险来自两个部分的组合:

不可靠的资源解析
+
高权限、低摩擦的自动执行

单独存在任何一个问题,危害都相对有限;两者连接起来,才形成完整攻击链。

为什么“先搜索再安装”仍然不充分

强制 Agent 在克隆或安装之前执行搜索,是有效的缓解措施,但不是资源身份验证。

在 HalluSquatting 论文的一组 Cursor CLI 实验中,执行搜索后有 93.4% 的资源解析结果正确;没有执行搜索时,99.1% 的结果是错误的。搜索明显降低了幻觉率,但搜索后的结果中仍有 6.6% 错误,而且 Agent 是否触发搜索还会受到模型、应用实现和用户措辞影响。

更根本的问题在于:

搜索解决的是“有没有一个结果”,而身份验证需要解决“这个结果是否就是目标资源”。

搜索结果是排序结果,不是信任证明

搜索引擎、代码托管平台和技能市场通常按照以下信号排序:

  • 名称相似度;

  • 文本相关性;

  • 下载量或星标数;

  • 更新时间;

  • 页面内容;

  • 用户行为。

这些信号可以提高相关性,却不能证明资源与用户所指对象具有同一身份。

攻击者注册一个名称完全匹配的资源后,它甚至可能比真正的资源更符合 Agent 的查询词。

HalluSquatting 研究展示了相应案例:对于一些显示名称和真实 slug 不一致的技能,规范资源没有进入搜索结果前十,而抢注的英文名称资源排在第一位。因此,“搜索后选择第一项”仍然可能稳定地选择攻击者资源。

资源存在,不等于资源真实

Agent 经常进行的是存在性检查:

HTTP 200?
仓库能否克隆?
软件包能否安装?
技能是否出现在市场?

但这些检查只能证明资源存在。

它们不能证明:

  • 所有者是预期开发者;

  • 仓库与用户提到的项目存在官方关系;

  • 当前内容与此前审核的内容一致;

  • 标签没有被移动;

  • 发布账户没有被接管;

  • 下载得到的制品来自声明的源代码。

搜索属于发现机制,不应同时承担信任判定。

防御重点:建立资源身份门

可靠的 Agent 不应直接从自然语言名称跳转到执行。中间必须存在一个独立于模型判断的资源身份门:

用户意图
→ 候选资源发现
→ 规范身份解析
→ 身份与版本验证
→ 权限审批
→ 隔离执行
→ 行为记录

一、验证所有者,而不只是项目名

对有权限安装代码、插件或技能的 Agent,应维护受信任所有者列表,例如:

允许:
github.com/llvm/*
github.com/oss-fuzz/*
github.com/organization-approved/*

需要人工审批:
github.com/unknown-owner/*

allowlist 不适合覆盖整个开放源代码生态,因为新项目和个人维护者不可能全部预先登记。更合理的策略是:

  • 已知所有者可进入自动验证流程;

  • 未知所有者可以被发现,但不能直接执行;

  • 新所有者首次使用时必须由人确认;

  • 确认结果写入策略库,而不是只保存在当前对话里。

这里验证的是身份,不是声誉。星标数、README 完整度和搜索排名都不能替代所有者确认。

二、使用规范化资源标识符

Agent 不应把显示名称、搜索词或自由文本 URL 直接当作资源身份。

仓库至少应规范化为:

platform + canonical owner + canonical repository

制品则应进一步包含:

registry + namespace + artifact name + digest

规范化流程需要处理:

  • 大小写和 Unicode 混淆;

  • URL 重定向;

  • 仓库转移或重命名;

  • fork 与上游仓库;

  • SSH、HTTPS 等不同 URL 表达;

  • 显示名称与实际 slug 的差异;

  • 非官方镜像和代理下载地址。

判断依据应来自平台 API、组织声明或经过签名的清单,而不是模型从名称中推断出来的 owner。

三、将版本绑定到 commit hash 或内容摘要

验证了仓库名称,只能说明 Agent 找到了正确的仓库,不能保证它执行的是正确版本。

分支和普通标签可能发生变化:

main
latest
v1
stable

审核时的 v1 和执行时的 v1 可能指向不同内容。

因此,执行单元应绑定到完整 commit hash、制品 digest 或等价的不可变内容标识。GitHub 在其安全指南中明确指出,对 GitHub Actions 而言,固定到完整 commit SHA 是使用不可变版本的可靠方式,同时还必须确认该 SHA 来自预期仓库,而不是攻击者的 fork。

资源记录可以采用类似结构:

{
  "canonical_uri": "https://github.com/owner/project",
  "owner": "owner",
  "revision": "full-commit-sha",
  "artifact_digest": "sha256:...",
  "approved_capabilities": [
    "read_workspace",
    "write_temp_directory"
  ]
}

需要注意,commit hash 证明的是内容固定,不证明内容安全。如果恶意版本本身已经被审核系统错误批准,固定 hash 只会稳定地执行恶意代码。

四、验证签名、声明和构建来源

对发布制品,仅验证仓库 commit 仍然不够,因为下载的二进制文件未必由该 commit 构建。

可以进一步验证:

  • commit 或 tag 签名;

  • 发布制品签名;

  • 构建 provenance;

  • 构建者身份;

  • 源代码 revision;

  • 制品 digest;

  • 签名者或 OIDC 身份是否符合策略。

Sigstore 的验证流程不仅检查签名,还会检查证书中的身份是否与预期身份匹配,并验证透明日志记录。SLSA provenance 则用于描述制品从何处、以何种构建过程生成,使消费者能够检查实际制品是否符合预期来源和构建条件。

但签名同样不是“安全证书”。

签名可以证明:

这份内容来自某个身份,而且签名后没有被修改。

它不能证明:

这份内容不存在后门。

因此,身份验证、内容分析和运行时隔离必须同时存在。

五、取消高风险动作的自动批准

对于以下动作,Agent 不应仅凭自己的判断自动批准:

  • 克隆未知仓库后执行脚本;

  • 安装新的技能、插件或 MCP Server;

  • 执行 curl | sh

  • 运行安装钩子;

  • 请求新的系统权限;

  • 访问 SSH 密钥、云凭据或浏览器数据;

  • 开放外部网络连接;

  • 修改持久化配置;

  • 向远程服务上传本地内容。

审批界面不能只显示:

是否允许执行安装命令?

至少应显示:

请求资源:Financial Search Engine
规范资源:financial-ai-analyst/mx-finance-search
实际所有者:financial-ai-analyst
解析方式:市场搜索第 3 项
版本:commit 8f...
签名:已验证 / 未验证
新增权限:网络、读取工作区
拟执行命令:...

用户需要审批的是实际动作,而不是 Agent 对动作的自然语言概括。

六、在沙箱中执行,并默认隔离凭据和网络

即使资源身份验证全部通过,也不能假设资源内容安全。

未知资源第一次执行时,应进入临时沙箱:

  • 使用一次性文件系统;

  • 默认不挂载用户主目录;

  • 默认不注入 API Key、SSH Key 和云凭据;

  • 网络默认关闭,按域名临时授权;

  • 限制子进程、系统调用和资源消耗;

  • 禁止访问 Agent 的长期记忆和完整对话;

  • 执行结束后销毁环境。

“禁止自动批准”与“沙箱执行”解决的是不同问题。

审批降低错误执行概率;沙箱降低一次错误执行能够造成的损失。两者不能互相替代。

七、记录资源解析和工具调用全过程

Agent 的日志不应只保留最终回答,还应记录:

用户提供的原始名称
搜索查询
候选资源列表
最终选择理由
规范 URL
所有者
commit hash 或 digest
签名验证结果
请求和获得的权限
执行命令
网络访问
文件变更
退出状态

这使安全人员能够回答:

  • Agent 为什么选中了这个仓库?

  • 它是否搜索过?

  • 搜索返回了哪些候选项?

  • 是模型、搜索排序还是策略引擎做出了最终选择?

  • 实际执行的内容与审核版本是否相同?

  • 哪些文件、凭据和网络端点受到影响?

没有这条证据链,发生问题后通常只能看到一个恶意进程,却无法还原它是如何从用户意图走到执行阶段的。

八、准确理解“可撤销执行”的边界

“执行能够撤销”是一个有用的设计目标,但不能被理解为所有后果都能回滚。

本地文件修改可以通过快照恢复,临时容器可以销毁,新增依赖可以删除。但以下行为通常不可撤销:

  • 已经发送到外部服务器的秘密;

  • 已经发布到互联网的内容;

  • 已经触发的支付或交易;

  • 已经发送的邮件和消息;

  • 已经调用的外部基础设施接口。

因此,更严谨的要求应是:

执行前可阻断,执行中可隔离,执行后本地状态可恢复;不可逆的外部副作用必须在执行前单独审批。

不要把“有回滚按钮”当作允许 Agent 获得无限权限的理由。

CodeQL 能发现什么,又发现不了什么

CodeQL 对 Agent 安全有价值,但它解决的是静态代码中的特定数据流和配置问题,不是完整的资源身份验证系统。

CodeQL 可以发现的部分

CodeQL 2.26.0 新增了 JavaScript/TypeScript 查询 js/system-prompt-injection。该查询追踪不可信输入是否流入模型的系统级提示、开发者级提示或工具描述,从而发现攻击者可能通过输入改变高优先级指令的代码路径。官方规则还建议,不要把用户输入直接拼接进系统提示或工具描述;必须使用时,应当用固定 allowlist 验证。

对于 GitHub Actions,CodeQL 还可以发现若干与供应链和执行路径有关的问题,包括:

  • 不可信输入进入 shell 或脚本;

  • 在高权限上下文中 checkout 不可信代码;

  • artifact 或 cache poisoning;

  • 使用可移动标签引用第三方 Action;

  • 工作流权限配置不当;

  • 已知易受攻击的 Action;

  • 不受控网络请求和部分路径注入。

这些查询能够识别代码中已经显式存在的危险数据流或工作流结构。

CodeQL 无法自动回答的部分

CodeQL 通常不能仅通过扫描应用源代码回答:

  • 用户所说的项目名实际对应哪个官方仓库;

  • 搜索结果第一项是否是真正目标;

  • 某个 GitHub 所有者是不是用户信任的维护者;

  • 模型为什么选择了候选 A 而不是候选 B;

  • 一个当前不存在的名称是否会在未来被攻击者注册;

  • Agent 运行时最终解析到了哪个重定向地址;

  • 安装时获取的制品是否与规划阶段检查的制品相同;

  • 市场排序、语义搜索和别名解析是否把请求导向了错误资源;

  • 运行时实际授予了哪些权限;

  • 人工审批界面是否隐藏了关键动作;

  • 下载后的 README 是否通过间接提示注入改变了 Agent 行为。

这些问题横跨模型规划、资源解析、平台身份、版本绑定、工具调用和运行时权限,不是单一源代码数据流查询能够完整覆盖的。

因此,CodeQL 可以发现“代码如何把不可信内容送进提示词或执行器”,却不能独立证明“Agent 获取的资源就是用户真正想要的资源”。

它是防御体系中的静态分析层,不是资源信任根。

真正需要保护的是从名称到动作的整条路径

过去的软件供应链防御往往从依赖文件开始:

package name → version → artifact

Agent 引入了一条更早、也更模糊的链路:

用户自然语言
→ 模型理解
→ 资源名称生成
→ 搜索与排序
→ 规范身份解析
→ 内容获取
→ 上下文摄入
→ 工具调用
→ 系统副作用

HalluSquatting 利用的正是这条链路中“名称生成”和“资源获取”之间缺少身份约束的问题。

攻击者不需要说服用户点击钓鱼链接,也不一定需要向 Agent 发送直接或间接提示注入。攻击者只需要预测模型会犯什么错误,再占据那个错误所指向的位置,等待 Agent 主动访问。

这也是 HalluSquatting 最重要的启示:

Agent 的问题不只是会不会被恶意文本说服。它还可能在攻击者没有直接接触用户的情况下,自主选择、获取并运行错误资源。

更复杂的提示词可以降低一部分错误,但提示词仍然由同一个概率模型解释,不能成为供应链信任根。

真正可靠的 Agent 至少必须满足三项条件:

资源来源可证明。
名称必须解析到规范身份,所有者、签名、来源和构建关系可以验证。

实际动作可见。
不能只展示 Agent 的自然语言计划,而要展示真实 URL、版本、权限、命令和网络副作用。

执行结果可追踪。
每次资源选择、验证、审批和工具调用都应形成可审计证据;本地状态应尽量可恢复,不可逆的外部动作必须提前阻断或审批。

资源身份验证不是 Agent 安全的全部,但它是第一道不能跳过的门。

因为在允许 Agent 判断一段代码是否恶意之前,系统首先必须确认:它拿到的究竟是不是原本打算检查的那一份代码。

Comments