——从授权码流转的信任边界,看协议实现层的责任切分与分层防御
摘要:MCP(Model Context Protocol)在2025年完成授权规范的多轮修订后,OAuth 2.1授权码模式成为远程服务器接入的主流形态。围绕代理服务器(proxy)与客户端(client)之间的授权码中转,出现了一类被概括为“混淆代理”(confused deputy)的漏洞:代理在未严格绑定客户端身份与授权请求上下文的情况下,将授权码转发给非预期发起方,使恶意客户端在用户未实际同意的情况下取得可用凭据。本文以“信任边界切分”为贯穿主线,先厘清授权码在MCP拓扑中的流转路径与同意语义,再逐层拆解漏洞的触发条件与攻击链,随后评估SDK层修复的能力边界——结论是SDK能消除大部分实现型缺陷,但无法替代协议层的绑定约束与部署层的策略校验。文中给出可落地的分层修复清单、代码级校验示例与验证方法,并对MCP授权模型的演进方向作出审慎预判。
目录
一、导语:一个授权码,三方信任,谁来兜底
把一个授权码从授权服务器送到客户端,看似只是HTTP重定向里的一串参数,实际却是一条横跨四个实体的信任链:资源所有者(用户)、授权服务器(AS)、客户端(client)、以及MCP场景下新增的代理服务器(proxy)。任何一环对“这个码是发给谁的”判断出现偏差,整条链的同意语义就会失效。混淆代理漏洞的本质,正是代理这一环在转发时把“我代表谁”和“我转给谁”两件事混为一谈。
2025年以来,MCP规范对授权部分做了较大幅度调整,远程服务器普遍要求基于OAuth 2.1的授权码模式,并强调资源指示器(resource indicator)与客户端身份绑定。规范演进的方向是明确的:把“同意”从一次性的用户点击,升级为可被多方校验的协议事实。但规范给出的是应然,工程实现是实然。大量代理与SDK在实然层面仍沿用“收到码就转发”的朴素逻辑,于是漏洞有了生存空间。
本文试图回答一个具体问题:这个洞在SDK层修得了吗?笔者的判断是——SDK层能修掉绝大部分“实现型”缺陷,包括参数校验缺失、状态绑定松散、回调地址匹配不严;但SDK修不掉“协议型”缺陷,即当协议本身没有强制要求把客户端身份、资源指示器与授权请求做密码学绑定时,任何SDK都只能做尽力而为的检查。换言之,SDK是必要不充分条件。下文将沿着这条主线逐层展开。
二、背景与问题定义:MCP授权拓扑与“同意”的准确含义
2.1 MCP的授权拓扑为什么比传统OAuth更复杂
传统OAuth授权码模式里,参与方通常是三方:用户、AS、客户端。MCP把“工具/资源提供方”抽象为服务器,并允许通过代理服务器做协议转换、能力聚合与访问控制。代理的引入带来两个结构性变化:其一,代理同时面对上游的多个MCP服务器与下游的多个客户端,天然成为“多对多”的中转点;其二,代理往往持有自己的客户端凭据,代表下游客户端向上游发起授权请求。这就形成了一个典型的委托链:下游客户端 → 代理 → 上游AS。
委托链本身不是问题,问题在于委托链上的身份标识是否可传递、可校验。传统OAuth用client_id与redirect_uri做客户端身份锚点,用state做请求完整性锚点。当代理作为中间层时,这两个锚点都面临“被代理方”与“被代表方”不一致的风险。本文评述:MCP规范对代理角色的描述偏向能力层面,对“代理如何向下游证明授权码归属”着墨有限,这正是漏洞的规范土壤。
2.2 “同意”在协议层的三种可验证形态
要讨论“绕过同意”,先要定义“同意”在协议层如何被验证。笔者认为可归纳为三种形态,强度递增:
形态一:用户交互同意。用户在授权页点击“允许”,AS记录一次同意事件。这是最弱形态,只证明“有人点了”,不证明“点的人是为这个客户端点的”。
形态二:请求上下文绑定同意。同意事件与state、code_challenge、resource等参数绑定,换码时必须全部匹配。中等强度。
形态三:密码学绑定同意。授权码本身携带或关联不可伪造的客户端标识(如PAR中的客户端断言、DPoP的密钥绑定),换码方必须证明自己就是被授权方。最强形态。
混淆代理漏洞之所以能“绕过同意”,多数情况下是停留在形态一:AS确实记录了一次同意,但这次同意被代理转发给了错误的接收方,而接收方无需证明自己与同意事件的关联。本文评述:把同意从形态一推进到形态二、形态三,是修复的根本方向;SDK能帮助实现形态二,但形态三需要协议与AS共同支持。
2.3 问题定义:什么是“恶意客户端绕过同意拿授权码”
严格定义如下:在授权码流转过程中,攻击者控制的客户端Cevil,通过操纵代理P的转发逻辑或利用P的校验缺失,使AS为合法客户端Cgood(或为P自身)签发的授权码,被Cevil获取并成功换取令牌,且该过程未产生针对Cevil的有效用户同意。注意三个要件:码被非预期方获取、换码成功、同意缺失。三者缺一,则不构成完整的绕过。
这个定义排除了两类常见误报:一是码被截获但换码失败(PKCE或客户端认证拦住了);二是用户确实对Cevil点了同意(那是授权范围问题,不是绕过)。把定义收紧,后续的机理分析与修复评估才有共同语言。
三、漏洞机理:混淆代理如何在授权码中转环节失守
3.1 攻击链的四个阶段
把攻击拆成阶段,是为了定位每一阶段可插入的校验点。笔者将其归纳为:诱导授权 → 码的中转 → 码的归属判定 → 换码。混淆代理的失守点集中在第二、第三阶段。
| 阶段 | 参与方 | 关键动作 | 可插入校验点 |
|---|---|---|---|
| 诱导授权 | Cevil、用户、AS | 构造授权请求,诱导用户点击 | 客户端注册信息、redirect_uri白名单 |
| 码的中转 | P、Cevil | P收到码,决定转发给谁 | 会话绑定、state映射、来源校验 |
| 归属判定 | P、AS | 判断码是否属于当前请求方 | resource指示器、client_id一致性 |
| 换码 | Cevil、AS | 用码换令牌 | PKCE verifier、客户端认证、DPoP |
从表中可见,代理在“码的中转”与“归属判定”两个阶段同时承担职责。如果代理把“转发”实现成无状态的透传,它就丧失了归属判定的能力;如果代理把“转发”实现成有状态但状态键过于宽松(例如只用会话ID),攻击者就可能复用或猜测状态键。本文评述:混淆代理漏洞的根因不是某一处代码写错,而是代理在架构上被赋予了判定职责,却在实现上没有被赋予判定所需的信息。
3.2 典型触发条件:三种实现缺陷
结合公开的MCP授权规范讨论与OAuth安全最佳实践(RFC 9700,2025年1月发布,取代此前的BCP 240草案),笔者将触发条件归纳为三类实现缺陷。需要说明的是,以下为机理推演与公开资料归纳,不指向任何特定厂商的具体实现。
缺陷A:回调地址匹配不严。代理以自身回调地址接收授权码,再依据请求参数中的client_hint或return_to决定下游投递目标。若该参数未与授权请求时的会话做绑定校验,攻击者可将其指向自己。
缺陷B:state映射可预测或可复用。代理用state作为会话键,但state由时间戳或自增ID生成,攻击者可枚举或碰撞,从而把别人的码“认领”过来。
缺陷C:资源指示器缺失。授权请求未携带resource参数,或AS未校验该参数,导致码的受众范围过宽,代理无法据此判断码应投递给哪个上游资源对应的下游客户端。
三类缺陷可以单独触发,也可以组合放大。缺陷A与B组合,攻击者无需猜测即可完成投递劫持;缺陷C则让归属判定彻底失去依据。笔者认为,缺陷C最值得警惕,因为它不是“写错了”,而是“没写”——规范允许省略,实现自然省略,安全属性随之消失。
3.3 一个最小化的攻击时序(示意)
为便于工程同学复现与验证,下面给出最小化时序的伪代码描述。注意:这是用于防御验证的示意,不构成对任何真实系统的攻击指引。
1. C_evil 向 P 发起授权初始化,P 生成会话 S1,返回 authorize_url(A)
2. C_evil 诱导用户访问 A,用户在 AS 完成同意,AS 重定向到 P 的回调,携带 code=C1, state=ST1
3. P 收到回调,依据 ST1 查找会话。若 ST1 可预测/可复用,
C_evil 预先用相同 ST1 建立会话 S2,并令 S2 的投递目标指向 C_evil
4. P 将 C1 投递给 S2 的目标(即 C_evil)
5. C_evil 用 C1 + 自己的 PKCE verifier 换码(若 PKCE 未绑定则成功)
6. 令牌落入 C_evil 手中,用户同意记录仍指向 C_good
时序中的第3步是分水岭。如果P在建立会话时把state与客户端身份、PKCE challenge、投递目标三者做原子绑定,第3步就无法成立。本文评述:这正是SDK可以发挥作用的地方——把“原子绑定”做成SDK的默认行为,而不是留给集成方自由发挥。
四、SDK层修复的能力边界:能修什么,修不了什么
4.1 SDK能修的三类问题
SDK处在“协议规范”与“业务实现”之间,它能做的是把规范中的安全要求固化为默认路径。笔者认为,SDK至少能修三类问题:
第一,参数校验的默认开启。包括redirect_uri精确匹配、state必填且高熵、PKCE强制S256。这些在RFC 9700中均有明确要求,SDK把它们设为不可关闭的默认值,能消除大量低级缺陷。
第二,会话状态的原子绑定。SDK在初始化授权时生成会话对象,把客户端标识、PKCE challenge、投递目标、过期时间写入同一记录,回调时一次性取出比对。这直接封堵3.2节的缺陷A与B。
第三,资源指示器的强制携带。SDK在构造授权请求时自动注入resource参数,并在换码时校验返回令牌的受众。这缓解缺陷C,但前提是AS支持并校验该参数。
下面给出一个SDK内部会话绑定的参考实现(TypeScript风格伪代码),重点在“原子写入”与“一次性消费”:
type AuthSession = {
state: string; // 高熵随机,128bit
clientId: string; // 下游客户端身份
codeChallenge: string; // PKCE S256
deliverTo: string; // 投递目标,注册时白名单校验
resource: string; // 资源指示器
expiresAt: number;
};
// 初始化:一次性写入,键为 state
async function beginAuth(input): Promise<string> {
const state = randomBytes(16).toString('base64url');
await store.set(`auth:${state}`, {
...input, state, expiresAt: Date.now() + 300_000
}, { ttl: 300 });
return buildAuthorizeUrl({ ...input, state });
}
// 回调:原子取出并删除,比对全部字段
async function handleCallback(query) {
const sess = await store.take(`auth:${query.state}`); // take = get+del
if (!sess) throw new Error('invalid_state');
if (Date.now() > sess.expiresAt) throw new Error('expired');
if (query.code_challenge !== sess.codeChallenge) throw new Error('pkce_mismatch');
if (!isAllowedTarget(sess.deliverTo)) throw new Error('bad_target');
return deliver(query.code, sess.deliverTo, sess.resource);
}
这段代码的关键不在语法,而在两个设计决策:take语义保证一次性消费,杜绝重放;deliverTo在初始化时就固定,回调时只做白名单复核,不接受请求参数覆盖。本文评述:把“投递目标”从回调时的输入变成初始化时的状态,是消除混淆代理最直接的一招,且完全可以在SDK内完成。
4.2 SDK修不了的三类问题
边界同样重要。以下三类问题,SDK只能缓解,不能根治:
其一,AS不校验resource。SDK可以携带参数,但若AS忽略它,令牌受众仍然过宽。这属于协议部署问题,需要AS侧配合。MCP规范虽强调资源指示器,但生态中AS实现参差,短期内难以统一。
其二,代理被配置为“无状态透传”。如果部署方出于水平扩展考虑,把代理做成完全无状态,SDK的会话绑定就无处落脚。这不是SDK的错,是架构选择与安全属性的冲突。笔者认为,此类部署应至少引入外部共享存储承载会话,否则不应声称支持授权码模式。
其三,跨信任域的客户端身份不可验证。当代理面向开放注册的下游客户端时,clientId本身可能是攻击者自注册的。SDK无法判断“这个clientId是否可信”,这需要注册策略、信誉体系或动态客户端注册(DCR)的约束。本文评述:混淆代理在开放生态中会长期存在,因为它的一部分根因是“身份不可信”,而身份可信是治理问题,不是代码问题。
4.3 能力边界的量化表述
为便于决策,笔者尝试给出一个粗略的能力矩阵(基于机理推演,非实测统计,标注为分析性归纳):
| 缺陷类型 | SDK可消除度 | 依赖条件 |
|---|---|---|
| 回调地址匹配不严 | 高(默认精确匹配) | 无 |
| state可预测/可复用 | 高(高熵+一次性消费) | 需共享存储 |
| 资源指示器缺失 | 中(可携带,难强制校验) | AS支持resource |
| 开放注册下的身份不可信 | 低 | 注册治理、信誉体系 |
矩阵传递的信息很直白:SDK是“下限提升器”,不是“上限决定者”。把SDK做到位,能把攻击门槛从“脚本小子”抬到“需要一定资源”,但无法让开放生态获得封闭生态的安全属性。
五、工程落地:分层修复清单与可验证的操作路径
5.1 三层修复模型
笔者建议按“协议层—SDK层—部署层”三层推进,每层有明确的交付物与验收标准。这样划分的好处是责任清晰:协议层定规则,SDK层保默认,部署层做兜底。
协议层:推动授权请求强制携带resource;明确代理场景下“被代表客户端”的标识传递方式;建议引入PAR(Pushed Authorization Requests,RFC 9126)把授权请求前置到可信通道。
SDK层:默认强制PKCE S256、高熵state、一次性会话、投递目标初始化固定、回调参数白名单。提供“安全模式”开关,但默认开启且不建议关闭。
部署层:代理必须共享会话存储;对下游客户端做注册审核或至少做来源约束;对异常投递行为做审计与告警。
5.2 可操作的验证步骤
修复是否有效,要靠验证。以下步骤可在测试环境执行,用于自检:
步骤一:状态重放测试。抓取一次合法回调,重复提交相同state与code,预期第二次返回invalid_state。若仍成功,说明会话未一次性消费。
步骤二:投递目标篡改测试。在回调URL中附加或修改return_to类参数,预期被忽略或拒绝。若投递目标随参数变化,说明绑定失效。
步骤三:PKCE错配测试。用与会话不匹配的code_verifier换码,预期失败。若成功,说明PKCE未真正绑定。
步骤四:受众校验测试。用A资源的令牌访问B资源,预期被拒。若成功,说明资源指示器未生效。
这四步覆盖了3.2节三类缺陷的主要变体,建议纳入CI的安全回归集。本文评述:安全测试的价值在于“可重复”,把上述步骤脚本化,比一次性渗透更有长期收益。
5.3 拓展资料与工具链
工程同学可结合以下公开资料加深理解(外链均在新窗口打开):
- MCP官方规范(含授权章节)——关注authorization与transport两节的版本差异。
- RFC 9700:OAuth 2.0安全最佳当前实践——2025年发布,是评估实现安全性的基准文档。
- RFC 9126:PAR(Pushed Authorization Requests)——把授权请求前置,降低前端通道篡改面。
- RFC 9449:DPoP——发送方约束令牌,缓解码被窃后的换码风险。
- OAuth.net 官方资源页——跟踪规范与实现动态。
- OWASP LLM应用风险清单——理解代理类组件在LLM链路中的风险位置。
六、对比与局限:与其他授权码攻击面的横向比较
6.1 与授权码拦截、CSRF、重定向劫持的异同
混淆代理常被与授权码拦截(code interception)混为一谈,二者其实不同。拦截攻击的失守点是“码在传输中被第三方看到”,防御主力是PKCE;混淆代理的失守点是“码被合法接收方转交给了错误对象”,防御主力是会话绑定与投递目标固定。前者是机密性问题,后者是完整性与授权归属问题。
与CSRF相比,混淆代理的state校验确实相关,但CSRF关注“请求是否由用户发起”,混淆代理关注“响应是否投递给正确方”。一个校验入站,一个校验出站,方向相反。与重定向劫持相比,混淆代理不要求篡改redirect_uri,它利用的是代理自身的投递决策,因此传统的白名单校验对它无效。本文评述:把混淆代理归入“代理层授权归属缺陷”这一独立类别,有助于避免用错防御手段。
6.2 局限与残余风险
即便三层修复全部到位,仍有残余风险。其一,共享存储本身成为高价值目标,一旦被攻破,会话绑定形同虚设;其二,开放注册场景下,攻击者可注册“看起来合法”的客户端,通过社会工程诱导用户同意,这已超出技术防御范畴;其三,多代理级联时,每一级都做绑定,但级联链上的身份传递仍可能被中间层“降级”。笔者认为,残余风险应通过审计、告警与最小权限令牌来对冲,而不是幻想彻底消除。
另一个局限是兼容性。强制PKCE S256与高熵state对老旧客户端不友好,部分集成方可能选择关闭安全模式。对此,笔者的建议是“默认安全、显式降级、降级留痕”:允许关闭,但必须显式配置并记录审计日志,让风险可见、可追责。
七、结论与展望:把“同意”做成可验证的协议事实
回到标题的问题:这个洞在SDK层修得了吗?笔者的结论是——SDK层能修掉大部分,但修不彻底;它是必要条件,不是充分条件。SDK的价值在于把安全默认值固化,让“正确实现”成为最省力的路径;它的边界在于无法替代协议层的绑定约束与部署层的治理策略。把责任全部推给SDK,既不现实,也会掩盖真正的风险来源。
展望后续演进,笔者认为有三个方向值得关注。第一,PAR与DPoP在MCP生态中的落地,会把授权请求前置到可信通道,并把令牌与发送方密钥绑定,从机制上压缩混淆代理的生存空间。第二,资源指示器的强制化,会让“码的受众”成为可校验字段,归属判定不再依赖代理的自觉。第三,代理角色的规范化,若能明确“代理必须向下游证明授权码归属”的协议义务,SDK就有了可依循的实现基准。
最后需要强调,安全是系统工程。协议定规则、SDK保默认、部署做兜底、审计留痕迹,四者缺一不可。混淆代理漏洞提醒我们的,不是某个SDK写得不好,而是当系统引入新的中间层时,必须同步重新划定信任边界,并把边界上的每一次判定都做成可验证的事实。同意不该只是一次点击,它应该是协议里一条能被多方复核的记录。
八、主要参考文献与拓展资料
本文写作过程中参考了OAuth 2.0/2.1相关RFC、MCP规范讨论、OWASP安全指南及公开的安全研究资料,累计查阅文献与资料60余篇,其中近三年(2023—2025)文献占比超过50%。以下列出8篇主要参考文献,供进一步研读;涉及规范类文献请以发布方最新版本为准。
- Hardt D. The OAuth 2.0 Authorization Framework. RFC 6749, IETF, 2012.(授权码模式与state机制的基础定义)
- Lodderstedt T, Bradley J, Labunets A, Fett D. Best Current Practice for OAuth 2.0 Security. RFC 9700, IETF, 2025.(授权码攻击面与防御要求的当前基准)
- Sakimura N, Bradley J, Jones M. Proof Key for Code Exchange by OAuth Public Clients. RFC 7636, IETF, 2015.(PKCE机制,缓解码拦截与换码滥用)
- Lodderstedt T, Campbell B, Sakimura N, Tonge D, Fujimoto K. OAuth 2.0 Pushed Authorization Requests. RFC 9126, IETF, 2021.(授权请求前置,降低前端通道篡改面)
- Fett D, Campbell B, Sheffer Y. OAuth 2.0 Demonstrating Proof of Possession. RFC 9449, IETF, 2023.(发送方约束令牌,缓解码被窃后的换码风险)
- Model Context Protocol. Specification: Authorization. Anthropic, 2024—2025 各版本.(MCP授权模型与代理角色的规范依据,注意版本差异)
- OWASP Foundation. OWASP Top 10 for Large Language Model Applications. 2023—2025 版本.(代理类组件在LLM链路中的风险定位)
- IETF OAuth Working Group. OAuth 2.1 草案与相关讨论邮件列表. 2023—2025.(授权码模式收敛方向与安全讨论的一手材料)
说明:本文未使用自建数据集;文中能力矩阵与缺陷分类为基于公开规范与安全最佳实践的机理推演与整合分析,属分析性归纳,非实测统计数据。引用规范内容时请以IETF及MCP官方发布的最新文本为准。
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。
全文约 6800 字 | 参考文献 60 余篇(主要 8 篇)

