用可运行靶场验证三类 MCP 安全缺陷
项目提供漏洞与修复两种模式,测试工具描述注入、令牌转发和资源归属校验缺失。作者报告修复后四项攻击检查均被阻断、正常控制检查结果不变,同时说明 Agent 使用桩实现,不能代表真实模型表现。
AI 深度解读
MCP 把能力包装成工具交给 AI agent,但工具能调用,不代表调用过程就安全。作者搭了一个可切换漏洞模式与修复模式的小型服务器,演示三种数据泄露路径:工具描述夹带指令、客户端令牌被转发、登录后越权读取别人的记录。
这篇最有用的地方,是把问题拆成了可以验证的检查:攻击是否成功,正常功能是否还在。不过,它验证的是一个受控实验,第一项甚至没有使用真实语言模型。读它适合拿检查思路,不能把测试通过当成安全认证。
三种漏洞,分别越过了哪道边界
第一种是工具投毒(Tool Poisoning):工具描述原本应该解释工具的用途,却夹带了要求 agent 执行额外操作的指令。
作者在 add_numbers 的描述里放入这类内容。测试中的模拟 agent 读到描述后,先按特定指令模式行动,再完成加法。结果是:漏洞模式下,用户的假密钥进入了模拟攻击者服务器的日志;修复模式下没有泄露。但两种模式都返回正确答案 5。
功能结果正确,不能证明执行过程没有泄露数据。
Invariant Labs 在 2025 年 4 月 1 日把这种攻击称为工具投毒,它属于间接提示注入:模型从外部内容中接收到了指令。OWASP 的对应分类是 LLM01:2025。工具描述尤其容易被忽略,因为 agent 会把它读进上下文,用户通常只看到简短展示。
对于自己开发的服务器,作者的修复是让描述只说明加法功能,并去掉工具不需要的额外参数。面对第三方服务器,这个办法就不够了:描述由对方控制,防线需要放到客户端。仓库建议固定已经审核过的描述,并向用户展示,但实验没有验证这项措施。
第二种是令牌透传(Token Passthrough):客户端给 MCP server 的令牌,又被原样拿去调用下游 API。问题不只是令牌有没有签名,而是它到底签发给谁使用。
第三种是对象级授权缺失:服务器知道调用者是 Alice,却没有检查目标发票是不是 Alice 的。认证回答「你是谁」,授权还要回答「这条记录是否允许你读取」。
这三类问题分别发生在指令、凭据和业务数据的边界上,修复位置也不同。
令牌验过了,为什么还会出问题
作者依据的是 MCP 2026-07-28 规范,并称已在 2026 年 11 月 13 日核对版本与引文。按原文引用的要求,MCP server 不得透传从客户端收到的访问令牌,也不得接受并非明确签发给自己的令牌。
这里的关键字段是 aud,即 audience,表示令牌预期交给哪个服务使用。一个令牌签名有效,只能说明它通过了相应的签名校验,不能说明所有服务都应该接受它。
实验中,两种模式都会检查入站令牌的 audience,差别发生在下一跳:
- 漏洞模式的
fetch_report直接转发客户端令牌,下游 API 只检查签名。 - 修复模式生成面向下游的新令牌,下游同时检查自己的 audience。
下面两段摘录展示了这两个修复点,分别位于令牌生成和下游校验的位置:
outbound = fa.mint(user, fa.AUD_DOWNSTREAM) # 服务器自己的令牌,audience 为下游服务
if args.mode == "fixed" and claims.get("aud") != fa.AUD_DOWNSTREAM:
return self._send(401, {"error": "wrong audience"})
要看的是 fa.AUD_DOWNSTREAM:发出的令牌指向下游,接收方也检查这个用途,不能只改其中一端就宣称整条链路已经验证。
测试以 Alice 身份调用 fetch_report,比较下游日志中的令牌与客户端令牌。漏洞模式下两者相同,客户端的 MCP 令牌直接请求下游也得到 HTTP 200;修复模式下两者不同,原令牌请求下游得到 HTTP 401。正常获取报告在两种模式下都成功。
不过,实验把「停止转发」与「下游校验 audience」一起修改,没有分别测试两项措施的效果。演示令牌也是带 sub、aud 声明的简化 HMAC 令牌,并非 OAuth。生产环境应通过令牌交换或服务凭据获取下游令牌,不能直接照搬共享演示密钥的签发方式。
原文还区分了规范中另一种 Confused Deputy Problem:涉及静态 client ID、动态客户端注册和授权同意 Cookie 的代理攻击。这个实验没有模拟它,不能把两者混成同一个已验证结论。
登录检查通过,为什么 Alice 还能读 Bob 的发票
get_invoice 在两种模式下都会拒绝没有令牌的请求。漏洞模式的问题,是取出发票后没有比较所有者与调用者。
修复只增加了一项记录归属检查:
if inv["owner"] != user:
raise PermissionError("not your invoice")
关键是 inv["owner"] != user。检查必须落在具体记录上,登录态本身不能替代它。
测试包含三个请求:Alice 读取 Bob 的发票、Alice 读取自己的发票、不带令牌读取。漏洞模式允许第一项,修复模式返回错误;后两项在两种模式下保持一致,分别成功和失败。
这属于 OWASP API1:2023 的对象级授权失效。对前端和全栈开发者,它对应一个很常见的接口问题:页面只展示当前用户的数据,但请求参数里的记录 ID 可以被修改。是否允许读取,最终必须由服务端根据调用者和目标记录判断。
作者还提到,他们此前的扫描器对比中,没有一种配置发现那组共六个授权测试样例。这个结果说明的是那次对比的边界,不能推导成所有扫描器都无效。但它指出了检测难点:工具需要同时知道两个账号,以及「哪条数据属于谁」的业务规则。
我的判断
这份实验最值得保留的是控制项。作者在 2026 年 11 月 12 日和 13 日各跑了一次,每次都是两种模式、八项检查,共十六个结果:漏洞模式四项攻击检查全部成功,修复模式全部阻断,三项正常使用检查的结果保持一致。工具描述扫描器则给出「检测到」与「干净」的结果。
它没有只证明「错误消失」,还检查了加法、获取报告和合法读取是否继续工作。比起只列风险名称,这种组织方式更容易转成团队自己的回归检查。
但证据强度需要分开看。工具投毒使用的是 stub,即只遵循某一种指令模式的模拟 agent,没有运行真实模型。它证明了描述能够进入上下文并驱动调用,没有证明某个真实模型会照做。原文另提到提示注入实验中,未防御时 300 次攻击有 69 次成功,成功率 23.0%;那不是本次 MCP 实验的数据,不能拿来当作这里的模型攻击成功率。
描述扫描器只有五条正则表达式,能识别这个样例,不足以抵挡精心改写的攻击。相较之下,审核并固定描述是在限制外部输入变化,正则扫描是在识别已知特征,两者解决的问题不同;前者在此也只有建议,没有对照数据。
fixed mode 通过的是这八项检查,不是整个 MCP 安全评估。
实验没有测试第三方或托管 MCP server,也没有覆盖工具返回内容中的注入、服务端请求伪造、状态句柄劫持、本地服务器失陷、OAuth 授权 URL 校验等其他问题。
复现信息倒是交代得具体:仓库为 mcp-security-fixture,提交为 194b4a6,运行环境是 mcp 2.3.0、uv 0.10.4、Python 3.13.12。最低要求是 Python 3.10 和 uv;run.sh 安装 mcp==2.3.0,在 127.0.0.1 的空闲端口启动服务并使用假数据,运行三个测试脚本后生成 results.csv。退出码 0 表示结果全部符合预期。环境已安装时,作者运行约需五秒。
另一个迁移细节是 Python SDK 2 将 FastMCP 改名为 MCPServer,1.x 的导入代码不能直接沿用。这些信息有助于复现,但运行速度并不能说明检测覆盖度。
能拿来做什么
- 给自研 MCP server 审核工具描述时,同时检查描述正文和参数列表。描述中面向 agent 下命令的文本、完成任务不需要的参数,都应追查用途;接入第三方服务器时,可以评估固定已审核描述的做法,并明确它尚未在这份实验中验证。
- 在 Node 或其他后端实现的 MCP 调用链里,追踪一次请求经过的每个服务。确认下游收到的是面向它的令牌,并检查下游是否拒绝 audience 不匹配的令牌,不能只确认入口处已经验签。
- 为按 ID 读取数据的工具准备两个测试账号,至少检查跨账号读取被拒绝、读取自己的记录成功、无令牌请求失败。把正常功能与攻击请求一起保留,才能判断修复是否破坏了合法使用。