返回 AICAP-180
B7 · Day 62OAuth 2.1 + MCP 安全 + CI gate

MCP Security Best Practices 2026-01

昨天(D61)我们把 MCP server 的身份钉成 OAuth resource server,画了 5×scope 矩阵。但「身份对了」不等于「实现安全」——具体会被怎么打?今天读 MCP Security Best Practices (2026-01) 这份权威,把三类典型攻击拆清,然后用 TDD 红阶段先固化「应该拒绝什么」的契约:写 6 条失败断言。这是 B7 从「设计」转入「可执行

阶段: B7 · OAuth 2.1 + MCP 安全 + CI gate(Day 61-70) 标签: #mcp-security #threat-model #tdd #token-passthrough

今日导引(由浅入深)

昨天(D61)我们把 MCP server 的身份钉成 OAuth resource server,画了 5×scope 矩阵。但「身份对了」不等于「实现安全」——具体会被怎么打?今天读 MCP Security Best Practices (2026-01) 这份权威,把三类典型攻击拆清,然后用 TDD 红阶段先固化「应该拒绝什么」的契约:写 6 条失败断言。这是 B7 从「设计」转入「可执行契约」的关键一跳——在写任何拦截代码之前,先让测试套件红着,逼实现去满足它。今天的「最小可判定产出」:6 条 red 断言 committed,每条注明命中哪类攻击。

1. 机理精读

MCP server 作为「代 client 调下游」的中间层,有三类典型攻击面,对应三条缓解:

① Confused deputy(被混淆的代理)。 server 自己往往持有比某个低权限 client 更高的权限(比如它有访问下游数据库的凭据)。攻击者诱使 server「用自己的高权限替低权限 client 越权调下游」。缓解:per-request 重新授权——server 不能因为「自己有权」就放行,每个请求都要把 client 的实际授权(token + scope)重新校验到下游动作上,不替 client 背书它本没有的权限。这正是 D61 audience 模型要解决的核心病。

② Token passthrough(令牌透传)。 server 收到 client 给它的 token,原样转发给下游 RS。问题:这个 token 的 aud 是为「当前 server」签的,不是为下游签的;如果下游不严格校验 aud 就接受了,audience restriction(RFC 8707)被整条绕过——攻击者用一个窄 token 撬开了一串服务。缓解:严格 audience 校验 + 拒绝透传——server 调下游时应换取下游专属的 token(token exchange),而不是把入站 token 直接甩过去;下游 RS 必须验 aud 命中自己才收。

③ Scope 越权(scope escalation)。 token 本身有效(签名对、没过期),但持有者用它调了超出 token scope 的工具。比如一个只含 aml:read 的 token 去调 sar.draft(需 aml:write)。缓解:调用前 scope 双校验——不是入口验一次 token 就放行所有工具,而是在每次 tool dispatch 前比对「该工具所需 scope ⊆ token 携带的 scope」。这是 D64「事中拦截」的理论依据。

三类攻击的共性边界:它们都不是「畸形输入」类攻击。schema 校验(JSON-RPC -32602 INVALID_PARAMS,见 toolRegistry.ts)只防参数畸形/类型错,完全不防越权——一个 aud 错误但格式完美的 token 能丝滑过 schema 校验。把 schema 校验当授权层,是 MCP 安全里最危险的认知错误(SOTA 段会再强调)。

三类攻击的 AML 具象化(把抽象攻击落到本计划的旗舰场景):

  • confused deputy:AML Copilot 的 server 持有访问案件库的高权限凭据。一个只该看公开类型学的外部 agent,诱使 server「用它的库凭据替我拉某客户的全部 KYC」。若 server 不把外部 agent 的实际 scope 重新核到下游动作上,就替它越权读了 PII。
  • token passthrough:编排层给 Copilot server 发了一个 aud=copilot 的 token,Copilot 转手调下游 sar-service 时把同一个 token 甩过去。sar-service 若不验 aud(它期望 aud=sar-service)就误收——一个本该止步 Copilot 的 token 撬开了 SAR 起草服务。
  • scope 越权:初筛 agent 的 token 只含 aml:read,被 prompt injection 诱导调 sar.draft(需 aml:write)。token 没过期、签名也对,唯一拦得住的是调用前 scope 比对。

为什么 2026-01 这份 best practices 是权威:MCP 早期(2025)spec 没有 OAuth 层,社区在 2025 H2 暴露了大量工具投毒/越权案例(MCPTox 基准显示工具投毒成功率最高 72%,见 toolRegistry.ts 注释),2026-01 的 best practices 把上述三类攻击 + 缓解收敛成可执行清单,是 D61-66 整套实现的事实标准。token passthrough 的正解是 token exchange(RFC 8693):server 调下游前拿入站 token 去 AS 换一个 aud=下游 的新 token,而不是原样转发。

2. 推导 / 手算 / 代码走读

把威胁清单映射成 6 条断言(TDD 红阶段契约)。 每条断言对应一类攻击的最小拒绝路径:

#断言(应失败/应拒绝)命中攻击类期望响应
1无 token 调工具 → 拒未认证401
2aud 的 token → 拒token passthrough / confused deputyreject(验签层)
3过期 token → 拒凭据失效reject
4scope 不足 → 拒scope 越权403
5token passthrough(把入站 token 甩给下游)→ 拒token passthroughreject
6越权工具(token 有效但工具超 scope)→ 拒scope 越权403

每条断言此刻都应该红——因为拦截逻辑还没接到 dispatch(D64 才接)。红 = 契约已固化,绿 = 实现满足契约。

这些断言落在哪个真实文件? D62 seed 计划写到 src/agent/mcp/__tests__/oauthGuard.test.ts。诚实标注:该路径的文件在当前 main 上不存在;本仓 OAuth 校验的真实测试实际落在 src/agent/__tests__/mcp/auth.test.ts(5 个 describe/it:round-trip、错密钥拒、过期拒、audience 限定、requireScope/bearerFromHeader)与 src/agent/__tests__/mcp/server.test.ts(真协议 in-memory)。也就是说,D62 设想的 6 条聚合断言,落地时被拆进了 auth.test.ts 的单元层 + server.test.ts 的协议层,而不是一个单独的 oauthGuard.test.ts。走读 auth.test.ts 里已绿的对应断言:

  • 'rejects a forged/tampered token (wrong secret)' → 覆盖断言 #2 的验签层。
  • 'rejects an expired token'(用 ttlSec: -10 铸过期 token)→ 断言 #3。
  • 'audience-restricts (RFC 8707) when required'audience: 'mcp-A''mcp-B' 应拒)→ 断言 #2 的 aud 维度。
  • 'throws insufficient_scope otherwise'requireScope(claims, 'admin') toThrow /insufficient_scope/)→ 断言 #4/#6 的 scope 维度。
  • 'parses the Bearer header ... rejects non-bearer' → 断言 #1 的「无/错 token」入口。

无 token → 401 这条则在 server.tsstartHttpServer 里实现(bearerFromHeader 返回 null → res.writeHead(401, { 'WWW-Authenticate': 'Bearer' })),由 server.test.ts 走真协议覆盖。

红→绿生命周期(B7 跨天的 TDD 节奏)

阶段测试状态含义
D62(今天)6 断言 fail契约固化:「应拒绝什么」写死
绿(内核)D63happy-path + 拒绝断言 passmint/verify/requireScope 实现满足契约
绿(接面)D64server 全绿 + 423 totalguard 接到网关,无 token→401 实测
语义打磨D65401 vs 403 拆分拒绝路径可被 agent 消费

今天只负责把红写对——红写错(断言太松)会让后面假绿。一个常见的红写错:断言 expect(call).toThrow() 而不指定错误类型,那么 handler 因任何原因抛错都过,无法证明它是因「越权」而拒。正确写法是 toThrow(/insufficient_scope/)(见 auth.test.ts),把拒绝原因也锁进契约。

3. 今日实战

  1. 阅读 MCP Security Best Practices (2026-01),逐条对照 D61 的威胁清单。
  2. 把 6 条「应拒绝」断言写成失败测试桩,每条注释标命中哪类攻击(confused deputy / token passthrough / scope 越权)。
  3. 提交红测试。注意:seed 命名的 src/agent/mcp/__tests__/oauthGuard.test.ts 在落地时被拆进了 src/agent/__tests__/mcp/auth.test.ts + server.test.ts——若沿用 seed 路径需在 PR 里说明合并去向,避免留下空文件。

4. 今日实测 / 产出

  • 6 条失败断言(6 red)committed——TDD 的红阶段,先固化「应拒绝什么」契约。已完成(红阶段)
  • 参考 MCP Security Best Practices (2026-01)
  • 诚实标注:6 条断言的最终落点是 src/agent/__tests__/mcp/auth.test.ts + server.test.ts(非 seed 命名的 oauthGuard.test.ts,该文件当前不存在)。绿阶段在 D63(happy-path)→ D64(接到 dispatch)逐步完成。

5. 常见误区 / 陷阱

  1. 把 schema 校验当授权层:JSON-RPC -32602 只防畸形输入(toolRegistry.tsvalidate),不防越权——aud 错的合法格式 token 照样过 schema。
  2. token passthrough 当「省事优化」:把入站 token 直接转发给下游,看似省一次取 token,实则整条绕过 audience restriction。
  3. 红阶段写成绿:TDD 红阶段断言若一上来就过,说明断言没真正约束「拒绝」语义(比如断言了「不抛错」而非「抛特定错」)。
  4. confused deputy 误判为「server 有 bug」:它不是实现 bug,是权限替代设计缺陷——server 不该用自己的权限替 client 背书。

6. 学习资源(每条带 YYYY-MM)

  • MCP Security Best Practices(modelcontextprotocol.io,2026-01)— 三类攻击 + 缓解的权威清单。
  • RFC 8707 Resource Indicators(IETF,2020-02)— audience 限定,反 token passthrough 的核心。
  • RFC 8693 OAuth 2.0 Token Exchange(IETF,2020-01)— server 调下游时换取下游专属 token,反 passthrough 的正解。
  • OWASP Top 10 for LLM Applications(OWASP,2025 版)— LLM/agent 越权与供应链攻击面。
  • MCPTox 基准(2026)— 工具投毒攻击成功率(见 src/agent/mcp/toolRegistry.ts 注释引用)。
  • NIST CAISI AI Agent 标准(NIST,2026-02 启动)— agent 工具调用的零信任要求。

SOTA检查 (2026-06 更新)

  • 当前主流MCP Security Best Practices (2026-01) 为当前权威,三类攻击(confused deputy / token passthrough / scope 越权)+ 三条缓解(per-request 重授权 / 严格 audience 拒透传 / 调用前 scope 双校验)是事实标准。
  • 是否仍 SOTA:是,但需在 2026-07-28 MCP spec 定稿后复查 RS 强制要求(metadata、token exchange)是否升级。
  • 过时黑名单:禁止把 schema 校验(-32602)当授权层——它只防畸形输入不防越权;禁止 token passthrough;禁止「入口鉴权一次后内部全信任」(D64 展开)。
  • 下次复查点:2026-07-28 spec 定稿当周,核对 best practices 是否随之修订;季度复查 MCPTox/OWASP LLM Top 10 新版。

衔接

  • 昨天:Day 61 — OAuth 2.1 资源服务器模型(RFC 9728/8707)。
  • 今天:拆清三类 MCP 攻击,TDD 红阶段固化 6 条「应拒绝」断言。
  • 明天:Day 63 — JWT 校验内核(jose),用 happy-path 绿掉 mint→verify 闭环。