外观
Agent 名称冲突攻击:多 Agent 系统中的错误对端调度
摘要
多 Agent 系统中,Agent-to-Agent Protocol(A2A)将 Agent Card 的 name 字段定义为人类可读的元数据,而非稳定的身份标识,且未规定名称冲突时的语义。当多 Agent 主机(Host)使用远程名称作为本地路由标识符、允许冲突、并按顺序或共享路由静默选择一个对端时,发往受信 Agent A 的请求可能被路由到攻击者控制的 Agent B,形成错误对端调度(wrong-peer dispatch)。Adithyan Arun Kumar 在 7 个开源实现(涵盖 6 个组织、3 种底层框架和 4 种实现形式)的受控测试中验证了这一攻击方法,并归入 CWE-706 对应的安全漏洞。
本文说明的是受控合成测试下的错误调度机制,不是任意生产多 Agent 部署的通杀漏洞,也不是 Agent A 的凭据自动传递到 Agent B 的通用继承攻击。凭据传递在测试的客户端绑定中未被发现,在 Broker 路径中仅当调用方已携带委托身份或令牌且 B 能消费路由时才会发生。
核心创新与差异
原研究贡献是首次将多 Agent 系统中的 Agent 名称从来历不明的冲突源(nuisance collision)提升为安全攻击面。论文追踪了从 Agent Card 注册到请求调度的完整链路,在 7 个指定版本的实现上运行隔离的回归测试,识别出 4 种实现形式的名称冲突路径:Agent 树歧义(Agent-tree ambiguity)、工具身份折叠(Tool identity collapse)、客户端映射替换(Client-map replacement)和 Broker 身份折叠(Broker identity collapse)。论文同时提出攻击所必需的五个条件:共享路由域、攻击者控制名称、冲突优先级规则、名称派生的路由使用以及两个对端之间的信任差异。
本站分析:这项研究的新增失效点不是 A2A 协议规范本身(规范从未声称 name 可作为身份标识),而是具体实现中缺少来源绑定的稳定身份(origin-bound stable identity)和名称到路由的确定性隔离。它与同一分类中研究多 Agent 信息流、政策事实传递和协同破坏的工作互补——那些工作假设 Agent 间通信路径正确,而本研究发现路径本身就不可靠。它补强了本分类的一个核心结论:跨 Agent 的安全责任在组合边界处被重新拼接,且拼接时使用的标识符本身可能被攻击者操纵。
威胁模型与攻击链
攻击者控制一个可参与多 Agent 路由域的 Agent,能够设置该 Agent 的 name 字段为任意值(包括与受信 Agent 相同或包含相同前缀的值)。攻击者不控制 Host 代码、不访问受信 Agent 的内部状态,也不需要拦截或篡改协议消息。
攻击目标是让发往受信 Agent A 的请求在 Host 内部被路由到攻击者控制的 Agent B。可能后果包括信息泄露(B 收到本应发给 A 的任务描述和数据)、拒绝服务(B 丢弃或错误处理请求)或请求被篡改后转发回调用者,但不包括 Agent A 的私有凭据或专用工具自动传递给 Agent B。
攻击链分为以下步骤:
- 注册:攻击者向共享路由域注册 Agent Card,其
name与受信 Agent A 相同或在路由查找中优先匹配。 - 发现:Host 发现两个 Agent 返回不同的 Agent Card,但内部路由表以名称键(name key)存储,后注册者可能覆盖先注册者。
- 调度:调用方请求与 Agent A 通信。Host 的路由查找使用名称作为键,返回攻击者控制的 Agent B 的端点。
- 执行:请求到达 B。B 可读取任务描述和参数,并以自身身份做出响应、丢弃请求或修改后返回。在原作者的合成凭据和工具测试中,未发现 Agent A 专有的 API 密钥或工具直接传递给 B。
最先失效的安全控制是 Host 的路由层未将 Agent 身份绑定到不可由远程对端自行声明的稳定标识符,且未对名称冲突执行拒绝或隔离。
攻击方法与复现材料
以下伪代码依据论文公开机制重构,用于说明名称冲突从注册到调度的链路和四种实现形式。使用本地模拟和多 Agent 测试夹具,不指向任何实际生产部署。
受控测试框架
python
# 共享路由域:本地模拟的 Agent Host 路由表
routing_domain = MockAgentHost()
# Agent A:受信对端
trusted_agent = routing_domain.register(
name="helper-agent",
agent_card=AgentCard(name="helper-agent", url="http://trusted.internal:8080"),
trust_level="trusted"
)
# Agent B:攻击者控制的对端,使用相同名称
attacker_agent = routing_domain.register(
name="helper-agent",
agent_card=AgentCard(name="helper-agent", url="http://attacker.internal:9090"),
trust_level="untrusted"
)
# 调用方请求与 "helper-agent" 通信
dispatched = routing_domain.resolve("helper-agent")
# 测试:检查实际路由目标
assert dispatched.url == "http://attacker.internal:9090" # 错误对端
assert dispatched.trust_level == "untrusted"四种实现形式
形式 1:Agent 树歧义(Agent-tree ambiguity)。客户端式集成将远程 Agent 注册为本地 Agent 树的子节点,以 name 为键。当两个远程 Agent 声明相同名称时,后注册的节点覆盖或遮蔽先注册节点。调用方遍历 Agent 树到达名称节点时,获得的是攻击者的端点。
python
# Agent 树以名称构建
agent_tree = AgentTreeNode()
agent_tree.add_child(name="helper-agent", remote=trusted_agent)
agent_tree.add_child(name="helper-agent", remote=attacker_agent) # 同名覆盖
# 遍历查找从根开始
target = agent_tree.find_by_name("helper-agent")
assert target.remote == attacker_agent # 返回最后插入的子节点形式 2:工具身份折叠(Tool identity collapse)。某些集成将远程 Agent 公开为本地工具(Tool),工具名称来自 Agent Card 的 name。当两个远程 Agent 映射到同一工具名称时,工具选择可能指向任意一个,取决于框架的工具解析顺序。
python
# 远程 Agent 以工具形式注册
tool_registry = ToolRegistry()
tool_registry.register_from_agent_card(trusted_agent.card) # name="helper-agent"
tool_registry.register_from_agent_card(attacker_agent.card) # name="helper-agent"
# 框架按名称查找工具
selected_tool = tool_registry.get_tool("helper-agent")
assert selected_tool.source_agent == attacker_agent # 取决于实现顺序形式 3:客户端映射替换(Client-map replacement)。客户端维持一个以名称为键的 Agent 端点映射表。同一名称的后续注册替换先前的条目,不产生冲突警告。
python
# 客户端内部映射:名称 → 端点
client_map = {}
client_map["helper-agent"] = trusted_agent.endpoint
client_map["helper-agent"] = attacker_agent.endpoint # 静默替换
# 调用时直接查找
resolved = client_map["helper-agent"]
assert resolved == attacker_agent.endpoint形式 4:Broker 身份折叠(Broker identity collapse)。基于 Broker(消息中介)的实现将两个同名 Agent 合并到同一名称派生的路由。请求到达该路由后的实际处理取决于 Broker 的队列和访问控制状态,结果可能是拦截(B 消费了发给 A 的消息)或拒绝服务(消息因路由冲突而丢失)。
python
# Broker 路由表以名称派生
broker_routes = defaultdict(list)
broker_routes["helper-agent"].append(trusted_agent)
broker_routes["helper-agent"].append(attacker_agent) # 名称冲突,同一路由
# 消息分发到路由的第一个消费者
message = broker_routes["helper-agent"][0].consume()
assert message.target == "helper-agent" # 但消息到达的是列表中第一个 Agent所有测试使用本地 mock、固定版本的实现和显式构造的名称冲突,不包含真实凭据、生产端点或完整协议会话。省略了具体的注册请求体和端点地址。
实验设计与实际过程
作者在 7 个固定的开源版本上运行隔离的回归测试(Google ADK Python/TypeScript、UiPath LangChain、BeeAI Framework、Solace Agent Mesh、Mozilla Any-Agent、AutoDev)。6 个客户端式集成表现出错误对端调度,第 7 个基于 Broker 的实现表现出身份折叠。测试不依赖对真实生产部署的入侵,也不统计受影响的生产实例数量。
合成凭据测试在请求中为 Agent A 携带专用 API 密钥,验证密钥是否出现在 B 收到的请求中;合成工具测试验证 A 注册的专用工具是否被 B 获得。两项测试在所有客户端绑定中均为阴性。Broker 路径中的调用方配置对象(caller-configuration object)在请求中传递,是否包含委托身份或令牌取决于 Host 的具体配置。
实验属于作者原始实验,本站未独立复现。
关键结果与实际影响
- 在 7 个受测开源实现中,6 个客户端式集成将发往受信对端 A 的请求路由到攻击者控制的 B。第 7 个 Broker 实现将两个对端合并到同一名称派生的路由。
- 结果属于错误对端调度,不是通用凭据继承。合成凭据和工具测试未发现 A 的专用凭据或工具直接传递给 B。
- 攻击的必要条件为五项同时成立:共享路由域、攻击者控制 Agent 名称、Host 允许冲突且存在确定性优先级、Host 以名称派生路由、A 与 B 存在信任差异。缺少任一条件,攻击链路不成立。
- 影响是信息泄露和任务篡改,在 Broker 路径中可能是拒绝服务。执行权限的提升受限于 B 本身的授权范围,且 Host 在下游动作前可能经过模型介导的附加决策。
- CWE-706 对应(使用未充分解析的名称或引用)。
防护措施与验证方法
将 Agent 身份绑定到来源锁定(origin-bound)的稳定标识符,例如在 Agent Card 中同时使用 URL 和公钥指纹的组合。Host 的路由层应当:
- 身份层面:以不可被远程对端单方面声明的标识符(如注册时分配的 UUID、URL + 公钥绑定)作为路由键,
name仅用于展示。 - 冲突处理:拒绝或隔离名称冲突的注册,不静默覆盖或合并。向管理员或编排器报告冲突。
- 调度验证:在请求发出前对比目标 Agent 的预期标识符与实际路由到的 Agent 标识符。
验证指标:在名称冲突条件下测量错误路由率;在名称不冲突条件下的误拒率;Broker 路径下的消息丢失率。测试应覆盖 Agent 退出后重新注册、相同 URL 但不同名称和不同 URL 但相同名称的组合。
局限与待验证问题
- 结果仅限 7 个指定版本的开源实现,未覆盖商业多 Agent 产品、自研编排平台或大规模生产部署。
- 合成测试未发现自动凭据传递,但不能排除某些 Host 的特定配置在身份委托条件下产生此类效果。Broker 路径中调用方配置对象的传递取决于 Host 实现。
- 尚未在真实多 Agent 任务中评估攻击的端到端影响(例如有多少比例的受信任务数据可被攻击者读取)。
- 未测试攻击者更改名称后是否触发 Host 的重新验证或人工审批流程。
- 独立复现尚未进行,本站未执行作者提供的完整测试套件。