阅读需 8 分钟

MCP 能力降级会削弱智能体控制吗?

MCP 能力降级需要安全测试,以证明授权、每次调用审核和防篡改审计记录在旧客户端上仍然有效。

MCP 能力降级会削弱智能体控制吗?

协议兼容性也是攻击面的一部分。当 MCP 客户端协商使用旧版本,或缺少某项能力时,它失去的应该是便利功能,而不是防止智能体擅自使用他人权限的控制措施。

我见过这种问题以兼容性补丁的形式出现,看起来毫无风险:客户端没有声明通知功能,于是代码进入旧分支;旧分支是在支持按会话授权之前编写的;由于没人把这条分支当作授权路径,某个操作就悄悄通过了。代码仍然能通过正常流程测试,但它依然是错的。

MCP 能力降级需要安全测试,因为初始化元数据会在智能体发出第一个敏感请求之前改变网关中的处理路径。要像攻击者一样测试这条路径:声明旧协议版本,省略字段,发送空的能力对象,重启进程,锁定保险库,撤销会话,然后检查审计日志之后记录了什么。如果结果从「拒绝或请求确认」变成了「直接执行」,兼容性就变成了凭据绕过。

降级改变的是路径,不是权限

兼容性路径可能改变消息格式、可用通知、进度处理方式,或客户端能收到的上下文数量。但它不能改变谁有权使用凭据,也不能改变是否必须由人批准调用。

在代码开始根据 protocolVersioncapabilities 分支之前,这个区别听起来很明显。开发者通常只是为了避免向不支持的服务器发送消息而编写分支。后来,有人因为会话设置、授权传递或审计初始化代码就在附近,就把它们也放进了同一分支。结果,降级意外改变了安全边界。

MCP 规范将初始化定义为一次交换:客户端声明协议版本和能力,服务器再返回自己的版本和能力。应把这些字段视为不可信对等方提供的声明。它们可以帮助双方实现互操作,但不能授予权限。

Sallyport 让这种分离更容易实现:它的保险库网关、会话授权和每次调用的密钥设置都位于 MCP 对话之下,因此智能体不能仅仅因为把自己描述成旧客户端就获得凭据。这样的架构方向正确,但仍需要在协议边界添加回归测试。

先写安全不变量,再写测试矩阵:

对于每一种被接受的 MCP 初始化形式,当保险库锁定时,受保护操作都会被拒绝;新的客户端进程需要会话授权;标记为每次调用审核的凭据每次使用都需要审核;无论操作成功、失败还是被拒绝,都会生成审计事件。

这句话比「旧客户端可以工作」更能帮助审查者做出判断。旧客户端只有在保留这些结果时才算正常工作。

初始化输入需要对抗性测试用例

行为正常的客户端会发送一次干净的初始化请求,列出自己支持的能力,并在收到响应后继续运行。安全测试应从这里开始,然后逐项移除这些假设。

使用一个能够发送原始 JSON-RPC 消息的小型客户端 fixture,不要只依赖会自动替你填充默认值的高级 MCP 库。库测试有用,但会隐藏引发降级问题的线路条件。

基准请求可能如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "CURRENT_TEST_VERSION",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": { "name": "compat-fixture", "version": "1.0" }
  }
}

然后让 fixture 生成只包含一个重要差异的变体:

  • 最旧的受支持协议版本
  • 当前版本,但将能力设为 {}
  • 当前版本,但省略 capabilities,前提是解析器接受这种形式
  • 缺少已知可选条目的能力对象
  • 服务器必须拒绝的不受支持旧版本

第一次测试不要把所有省略情况组合在一起。组合后的格式错误请求失败时,你几乎无法判断原因。一次只改变一个变量,才能知道是哪个默认值或分支改变了结果。

对于每个被接受的用例,在授权前断言受保护操作得到相同的结果。具体传输响应会因实现而异,但结构必须清晰明确:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "Session authorization required"
  }
}

不要只断言错误文本。还要断言稳定的错误类别或内部结果代码、没有发出的 HTTP 请求或 SSH 调用,以及针对这次拒绝尝试生成的审计记录。表面上返回友好错误,背后却产生了真实副作用,这样的测试就是失败的。

还有一种尴尬情况:初始化请求中的字段都能解析,却彼此不合逻辑。比如客户端声称使用服务器认识的版本,却没有声明该版本正常客户端会使用的任何能力。网关无需训斥客户端,而是要选择安全路径:接受受限会话并保留所有本地控制,或拒绝初始化。不能因为通常的授权展示方式不可用,就默默选择不受保护的路径。

旧线路格式不能创建旧的安全模型

团队常说支持某个旧协议版本,实际意思往往只是仍然能解析它的消息。这只完成了一半工作。还需要决定,当旧线路格式没有对应字段时,当前的哪些安全行为仍然必须保留。

通常答案是全部保留。会话授权是本地状态,每次调用审核是本地状态,审计记录也是本地状态。这些都不需要客户端在旧请求中携带等价字段。

能力协商和授权经常在这里混为一谈。客户端可能缺少接收结构化状态更新的功能。这说明应如何传达授权状态,不代表可以跳过授权。如果产品无法为这次调用向用户展示审核,应该以清晰的错误拒绝调用。要求客户端保证自己显示过提示,不能替代本地决策。

避免使用「如果客户端无法处理每次调用提示,就批准整个会话」这种常见回退方案。它受欢迎,是因为能减少阻碍,让演示顺利通过。但它把凭据所有者对每次使用的决定,未经同意改成了一次性决定,这是错误的。应以凭据的设置为准。

让版本适配器保持狭窄。它应转换请求和响应语法,过滤对等方无法理解的消息,并映射兼容的错误形式。它不应决定是否执行操作。把授权放在一条所有适配器都会调用的路径上。

代码审查时可以问:兼容性分支能否在没有先通过与最新客户端相同的授权函数时,返回执行句柄、注入授权请求头或 SSH 结果?如果可以,这条分支现在就需要测试,之后还需要尽快重新设计。

会话授权必须跟随发起请求的进程

按会话授权很容易测错。如果测试客户端一直运行,获得授权后连续执行十次调用,你只能证明一个正常会话会继续保持授权,无法证明下一个智能体进程需要自己的决定。

使用两个独立启动的 fixture 进程。让它们拥有相同的显示客户端名称、相同的协议版本和相同的能力声明。进程 A 初始化并请求受保护操作,为它批准会话。然后让进程 B 初始化并请求完全相同的操作。

进程 B 必须收到新的授权要求。它不能因为与 A 共享客户端标签、工作目录、传输端点或缓存的连接信息,就继承 A 的授权。如果实现会在授权卡片中展示代码签名权限,请断言卡片标识的是实际启动 B 的进程权限。不要让测试断言卡片上的装饰性内容,而要断言人类做决定时真正使用的身份信号。

然后测试生产环境中的智能体会遇到的生命周期边界:

  1. 批准进程 A,让它正常退出。使用完全相同的元数据启动进程 B。B 必须需要授权。
  2. 批准 A,然后不进行正常关闭就终止它。启动 B。B 必须需要授权。
  3. 批准 A,在它仍然运行时从会话日志中撤销 A,然后让 A 再次调用。该调用必须在执行前停止。
  4. 当 A 的第一次操作正在等待外部响应时撤销 A。待处理操作不能在撤销后创建第二个已批准操作。

第三和第四种情况能暴露一个常见错误:系统只在打开连接时,或只在创建操作对象时检查授权。进程仍在运行时,会话也可能发生变化。应在网关决定提交外部操作的那一刻检查权限。

不要使用调用方提供的会话 ID 作为授权密钥。可以这样测试并暴露缺陷:让 B 重放 A 的会话标签,证明网关仍然会请求授权。客户端名称只是用于显示和诊断的标识,不能证明是同一个可执行程序再次发起请求。

每次调用审核都属于凭据使用本身

让凭据远离 MCP
智能体使用内置的 sp mcp shim,Sallyport 注入凭据但不会暴露它们。

每次调用密钥设置意味着网关每次使用该凭据时都请求同意。它不意味着「每个工具名称一次」「每个打开的连接一次」,也不意味着「除非客户端很旧,否则只需要一次」。需要审核的是将携带秘密或使用 SSH 密钥的真实请求。

建立一个测试,使用标记为每次调用审核的凭据,并让 fixture 在已经获得授权的同一会话中发送两个等价操作。对于 HTTP,向返回无害标记的测试端点发送两次请求。对于 SSH,向隔离的测试主机执行两条无害命令。测试应观察到两次独立的审核事件和两条独立的操作记录。

最小的预期事件顺序如下:

session_authorized process=fixture-A
call_review_requested action=41 credential=deploy-token
call_completed action=41 result=success
call_review_requested action=42 credential=deploy-token
call_completed action=42 result=success

重要的断言不是编号,而是操作 42 不能使用授予操作 41 的授权。让第二个请求在第一个请求完成后到达,然后再增加一个两个请求几乎同时到达的用例。并发会暴露这样的实现:它把一个临时的「审核通过」标志存放在会话上,而不是绑定到单个操作。

现在用旧协议声明重复测试,并省略正常授权状态路径使用的能力。审核机制在本地的展示方式可能不同,但结果必须保持不变。如果用户关闭、取消或等待第二次审核超时,测试端点必须只看到一个请求,而不是两个。

对秘密暴露保留硬性断言。MCP 响应可以包含操作结果、错误或需要审核的状态,但不能包含 API 密钥、SSH 私钥、代表秘密的脱敏占位符,或能让智能体重建秘密的工具参数。降级路径很容易诱使兼容性适配器序列化额外上下文。应检查原始传输记录,而不只是结构化测试对象。

审计记录应位于协议协商之下

只记录现代客户端成功调用的审计日志,只是一种自我安慰。以后真正需要查看的,往往是被拒绝的调用、取消的审核、被拒绝的初始化请求,以及那些让工程师说「这不应该发生」的异常兼容路径。

记录重建决策所需的事实:智能体运行或会话、操作身份、通道、请求目标、凭据引用或安全标识符、授权状态、审核结果和执行结果。不要保存秘密本身。对于初始化失败,应记录足够的对等方和解析上下文来解释拒绝原因,但不要把不可信载荷的副本变成审计日志。

Sallyport 从一个加密的哈希链式审计日志生成 Sessions 和 Activity 日志。这种设计对降级测试很重要,因为客户端能力不应决定是否存在第二套日志。相同的写入路径应该能处理当前客户端、能力不完整的客户端和被拒绝的调用。

在每个端到端用例结束后,用离线链验证作为一项断言:

$ sp audit verify
verified: 18 records
chain: intact

具体措辞可以不同,但测试必须要求验证成功,而且不能打开保险库。这个测试能发现链式写入被破坏或被省略,但不能证明写入了正确事件。因此还要查询 Sessions 和 Activity 视图,检查预期的拒绝、批准、取消或完成记录。

也要测试事件顺序。如果审核被拒绝,日志应显示请求和拒绝,但不能显示伪造的完成记录。如果传输操作在授权后失败,应将其记录为执行失败,而不是授权拒绝。这是两个不同的运维问题。把它们混在一起会让事件调查变得困难,也会让有问题的适配器看起来像是用户做出的选择。

围绕安全结果构建兼容性矩阵

让降级路径留在保险库之下
加密保险库锁定时,它会拒绝所有 HTTP 和 SSH 操作。

把每个版本、能力、通道和凭据设置做成完整笛卡尔积,测试数量会不断增长,最后没人运行。保留一个覆盖每项安全决策的小型必需矩阵,并在出现新的适配器分支时增加用例。

用行表示初始化形式,用列表示本地决策状态。例如,让最旧的受支持客户端、空能力客户端和正常的当前客户端,分别在锁定的保险库、未授权会话、已授权会话凭据和每次调用审核凭据下运行。如果 HTTP 和 SSH 共用同一授权路径,但使用不同的执行辅助程序,就让每个重要单元都经过这两个通道。

测试运行前,先用直白的语言写出预期结果:

客户端条件本地状态预期结果
最旧的受支持版本保险库锁定操作被拒绝并记录
能力为空保险库已解锁,新进程请求会话授权并记录
旧版本进程已授权,每次调用密钥每次操作都请求每次调用审核
不受支持的版本任意状态拒绝初始化,不执行外部操作
当前版本进程已撤销操作被拒绝并记录

这张表之所以重要,是因为它迫使团队决定如何处理不受支持的版本。协商失败后,不要让服务器「尽力尝试」。这句话通常意味着服务器会在测试远少于受支持路径的解析器路径上执行请求。应拒绝会话,安全地记录拒绝,并要求使用受支持的客户端。

对于每一行,收集三项独立观察结果:原始 MCP 响应、虚假 HTTP 服务或测试 SSH 主机上的观察结果,以及审计结果。仅凭错误响应无法证明操作没有到达外部世界。仅凭测试端点也无法证明网关记录了拒绝。三者缺一不可。

失败记录应明确指出损坏的边界

兼容性测试失败时,不要将问题记录为「旧版 MCP 客户端有问题」。这样的描述会让下一个开发者去修复展示层,却漏掉授权问题。

应写出不变量和时间线。下面是一份值得处理的失败报告:

Client B initialized with oldest accepted version and no optional capabilities.
Client A had already received session approval and was still running.
Client B sent an HTTP action using the same displayed clientInfo name.
Gateway injected the credential and the test endpoint received the request.
No approval card appeared for B.
Activity journal recorded completion, but Sessions journal contained only A.

这段顺序说明,缺陷在于进程身份或授权缓存范围,而不是版本解析。它也说明审计日志中是否存在可能误导调查人员的记录。应在这一层添加回归测试,然后继续向内追踪,直到实现中只有一个授权检查,而且 B 无法绕过它。

另一种失败模式更加隐蔽:

Client initialized without the capability used for approval status updates.
Credential required per-call review.
First action displayed local review and completed.
Second action completed immediately.
Audit log recorded two completed actions and one review event.

这说明审核令牌逃出了操作作用域。修复方法不是禁止旧客户端,而是将已批准的审核绑定到操作 nonce 或内部操作记录,并且恰好消费一次。

这些记录也能帮助支持团队区分用户决定和软件故障。如果日志显示用户拒绝了审核,就应调查请求。如果日志显示需要审核的凭据从未触发审核,就不能再把这个事件当作正常的智能体行为。

锁定和撤销是两项独立测试

保护旧客户端上的 SSH
SSH 操作通过内置的 sp-ssh helper 执行,因此智能体不会持有 SSH 密钥。

保险库网关、会话授权和每次调用审核回答的是不同问题。降级测试套件应分别测试它们,因为通过授权测试可能掩盖保险库锁定失败,反过来也一样。

先锁定保险库。启动每个兼容性 fixture,包括最旧的受支持版本。每个受保护操作都必须在注入凭据前失败,并且外部服务不能看到任何请求。不能接受仅仅要求智能体自行提供凭据的结果,智能体绝不能获得这些凭据。

然后解锁保险库,但保持会话未授权。fixture 应在这次运行中进入一次会话授权流程。批准它,执行一个受保护操作,然后在进程仍然运行时撤销会话。下一次操作必须失败,即使保险库仍处于解锁状态。

最后,在这个已授权会话中使用需要每次调用审核的凭据。批准一个操作,拒绝下一个,然后检查日志。这一序列可以证明决策链保持正确顺序:保险库锁定时阻止所有操作;会话授权为该进程建立权限;凭据设置可以在此基础上要求新的决定。

不要把这些测试合并成一个很长的场景,除非你同时保留针对每个边界的聚焦测试。长端到端测试适合证明某条路径存在,却不擅长说明重构后它为什么中断。

发布门槛应严惩静默成功

当兼容性问题让操作悄悄发生时,就应阻止发布。状态消息的小幅不一致可以留到下一个补丁修复。降级客户端继承授权、跳过每次调用审核、在保险库锁定时执行操作,或从审计记录中消失,这些都跨越了安全边界。

凡是改动 MCP 初始化、协议适配器、授权传递、进程跟踪、凭据注入或审计投影的提交,都应在持续集成中强制运行这些测试。不要等到协议版本升级才测试。真正的风险来自围绕缺失能力进行分支,而普通功能开发一直会创建这类分支。

最能体现测试价值的,通常是那个最难看的用例:旧客户端声明、空能力对象、重复使用的显示名称、第二个进程,以及第一次批准后对凭据的第二次使用。保留它。只有这样,才能找到那些看似友好的兼容代码,它们悄悄把人工决定变成了缓存命中。

常见问题

应该测试多少个旧版 MCP 协议?

测试你仍然声称支持的版本,以及兼容代码可能意外接受的最旧版本。然后测试一个声明当前版本、却省略可选能力字段的客户端。第二种情况能发现普通旧客户端测试容易漏掉的解析和默认值问题。

MCP 客户端能力可以移除授权提示吗?

不能。客户端的能力声明只说明它能做什么或能显示什么,不能决定操作网关是否检查身份、请求审核或记录操作。

如果 MCP 客户端发送格式错误的能力声明,应该怎么办?

干净地拒绝格式错误的初始化消息,在会话完成有效的初始化和授权流程前,拒绝所有受保护操作。不要猜测格式错误的客户端想做什么。宽松的解析器其实是在用兼容性伪装授权决策。

如何测试会话授权无法被重复使用?

如果授权绑定到会话,就应将它绑定到实际客户端进程或同等强度的运行时身份,而不是调用方提供的会话标签。即使新进程使用相同的客户端名称并请求相同的工具,也必须获得自己的授权。

不支持通知的客户端应该支持每次调用授权吗?

审核应绑定敏感操作和凭据策略,而不是客户端声明的通知功能。如果客户端无法显示正常的交互式审核,网关应使用本地审核路径,或拒绝操作。静默回退是不安全的选择。

为什么降级客户端仍然必须生成审计记录?

因为依赖能力进行日志记录,会在兼容代码采用异常路径时造成盲区。应沿用当前客户端使用的同一审计路径,记录调用尝试、授权结果、目标身份和执行结果。

离线审计链验证能证明什么?

它能证明加密哈希链没有被篡改或破坏,但不能证明每个预期测试事件都已记录。应将验证与事件存在性断言结合起来,并检查事件中的授权和结果字段是否正确。

MCP 降级测试需要端到端覆盖吗?

需要,只要测试工具能够作为真实客户端进程运行,并检查本地授权和审计界面。单元测试能发现解析器回归,但通常无法证明进程身份、人工决策、执行过程和日志记录仍然彼此关联。

缺少 MCP 能力会自动构成安全问题吗?

不需要。缺少可选能力最多只能降低便利性,绝不能让受保护调用变成未经审核的调用、抹掉调用尝试的记录,或让客户端退出后权限继续有效。

哪些降级测试失败应阻止发布?

凡是旧版或能力不完整的客户端在缺少授权后仍能使用凭据执行、绕过每次调用审核、继承其他进程的会话授权,或无法生成防篡改审计事件,都应阻止发布。展示上的细微差异可以稍后修复,这些问题不能延后。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov