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 |
| 2 | 错 aud 的 token → 拒 | token passthrough / confused deputy | reject(验签层) |
| 3 | 过期 token → 拒 | 凭据失效 | reject |
| 4 | scope 不足 → 拒 | scope 越权 | 403 |
| 5 | token passthrough(把入站 token 甩给下游)→ 拒 | token passthrough | reject |
| 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.ts 的 startHttpServer 里实现(bearerFromHeader 返回 null → res.writeHead(401, { 'WWW-Authenticate': 'Bearer' })),由 server.test.ts 走真协议覆盖。
红→绿生命周期(B7 跨天的 TDD 节奏):
| 阶段 | 天 | 测试状态 | 含义 |
|---|---|---|---|
| 红 | D62(今天) | 6 断言 fail | 契约固化:「应拒绝什么」写死 |
| 绿(内核) | D63 | happy-path + 拒绝断言 pass | mint/verify/requireScope 实现满足契约 |
| 绿(接面) | D64 | server 全绿 + 423 total | guard 接到网关,无 token→401 实测 |
| 语义打磨 | D65 | 401 vs 403 拆分 | 拒绝路径可被 agent 消费 |
今天只负责把红写对——红写错(断言太松)会让后面假绿。一个常见的红写错:断言 expect(call).toThrow() 而不指定错误类型,那么 handler 因任何原因抛错都过,无法证明它是因「越权」而拒。正确写法是 toThrow(/insufficient_scope/)(见 auth.test.ts),把拒绝原因也锁进契约。
3. 今日实战
- 阅读 MCP Security Best Practices (2026-01),逐条对照 D61 的威胁清单。
- 把 6 条「应拒绝」断言写成失败测试桩,每条注释标命中哪类攻击(confused deputy / token passthrough / scope 越权)。
- 提交红测试。注意: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. 常见误区 / 陷阱
- 把 schema 校验当授权层:JSON-RPC
-32602只防畸形输入(toolRegistry.ts的validate),不防越权——aud错的合法格式 token 照样过 schema。 - token passthrough 当「省事优化」:把入站 token 直接转发给下游,看似省一次取 token,实则整条绕过 audience restriction。
- 红阶段写成绿:TDD 红阶段断言若一上来就过,说明断言没真正约束「拒绝」语义(比如断言了「不抛错」而非「抛特定错」)。
- 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 闭环。