MCP发布了其授权规范。大多数构建MCP服务器的开发人员要么完全忽略认证,要么简单地添加一个JWT中间件并认为问题解决了。规范比这要复杂得多——而且比大多数人意识到的威胁面要大得多。
基础知识(如果你已经了解OAuth可以跳过)
MCP使用OAuth 2.1进行远程服务器认证。流程标准:客户端向你的服务器发送请求,收到401状态码并带有WWW-Authenticate头部指向受保护资源元数据文档。该文档告诉客户端应该与哪个授权服务器进行通信。客户端发现授权服务器的端点,进行动态或预注册,用户在浏览器中进行授权同意,客户端获得令牌并使用它。
对于本地STDIO服务器,这一切都不适用——你可以使用环境变量、嵌入的凭据等,具体取决于情况。OAuth适用于基于HTTP的传输,其中服务器远程托管。
有趣的部分不是成功的流程。而是出错的地方。
混淆的副手——最大的威胁
这是规范花费最多篇幅讨论的攻击,而且是有充分理由的。
想象一个MCP代理服务器——一个位于MCP客户端和第三方API之间(例如:一个将GitHub API或Slack API包装起来的MCP服务器)。代理使用静态客户端ID与第三方授权服务器进行通信。MCP客户端动态注册到代理,每个客户端获得自己的客户端ID。
以下是攻击过程:
- 用户通过代理合法地访问第三方API进行认证。第三方授权服务器设置了一个同意cookie。
- 攻击者动态注册一个恶意MCP客户端到代理,设置
redirect_uri为attacker.com。 - 攻击者向用户发送一个精心构造的链接,触发通过代理的授权请求。
- 用户的浏览器仍然保留了步骤1中的同意cookie。第三方授权服务器看到cookie,跳过了同意屏幕。
- 授权代码被重定向到攻击者的服务器。
- 攻击者用代码换取访问令牌。游戏结束。
修复方法是在MCP服务器级别进行每个客户端的同意——在请求到达第三方授权服务器之前。MCP代理需要有自己的同意页面,通过名称识别请求的MCP客户端,展示它想要的权限范围,显示重定向URI,并需要明确的批准。该同意决定在服务器端存储,与特定的client_id关联。
规范还要求OAuth state参数是一次性的、短暂的,并且至关重要的是——在用户批准MCP级别的同意屏幕之前,状态cookie不得设置。在批准之前设置它会完全失去其意义。
令牌转发——明确禁止
一些MCP服务器开发人员认为服务器的任务是接收客户端的令牌并将其转发到下游API。规范非常明确:不要这样做。
如果你的MCP服务器接受其他服务颁发的令牌并将其转发,你就在协议级别创建了一个混淆的副手。下游API会像你的服务器验证了令牌一样信任它。你的服务器的速率限制、请求验证和审计记录都变得毫无意义,因为令牌从未打算给你用。
始终验证aud声明是否匹配你的服务器资源URL。如果令牌不是为你颁发的,拒绝它。
OAuth发现期间的SSRF
这个威胁很微妙,容易被忽略。
在OAuth发现过程中,MCP客户端从多个来源获取URL:WWW-Authenticate头部中的resource_metadataURL,PRM文档中的authorization_serversURL,以及授权服务器元数据中的各种端点。
恶意的MCP服务器可以将这些字段填充为指向内部资源的URL:
http://169.254.169.254/latest/meta-data/— 云凭证http://192.168.1.1/admin— 内部服务http://localhost:6379/— 本地Redis
客户端在发现过程中盲目地跟随这些URL。它成为一个SSRF代理。
缓解措施很简单:强制使用HTTPS(开发中允许回环地址除外),阻止私有IP范围(10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16),验证重定向目标时使用相同的限制,并考虑在服务器端部署时使用出口代理。DNS重绑定也是一个问题——在检查和使用之间锁定解析。
OAuth URL注入
在授权流程中,MCP服务器提供授权URL,客户端在浏览器中打开。恶意服务器可以提供javascript: URL而不是https:// URL。如果客户端未经验证就传递给window.open(),你将面临XSS。如果客户端使用shell命令打开URL,你将面临命令注入。
结合通过标准输入输出(stdio)架构启动MCP服务器的代理架构,XSS升级为宿主系统的任意代码执行。攻击者从客户端的环境中提取代理认证令牌,通过代理进行认证请求,代理执行攻击者想要的任何命令。
修复方法:只允许http://和https://方案。永远不要使用shell命令打开URL。使用CSP头作为纵深防御。
范围最小化——无聊但重要的部分
大多数MCP服务器在scopes_supported中发布它们支持的所有范围,客户端请求所有范围。用户在同意屏幕上看到所有内容,点击批准,获得具有最大权限的令牌。
如果令牌泄露——日志暴露、内存刮取、本地拦截——攻击者将拥有访问一切的权限。撤销它会中断所有工作流程。
更好的方法是渐进式提升。从最小的范围开始(mcp:tools-basic用于只读发现)。当客户端尝试需要更多访问权限的操作时,服务器返回带有特定范围的WWW-Authenticate挑战。客户端仅为此范围重新授权。令牌的辐射范围保持较小。
这比“一次性给我所有权限”更难实现,但区别在于被泄露的令牌影响一个操作还是所有操作。
我真正担心的是什么
如果今天我要构建一个MCP服务器,我的优先级列表如下:
- 受众验证。验证
aud声明是否匹配你的服务器。这是最重要的检查。如果没有它,任何来自同一授权服务器的令牌都有效。 - 代理服务器的每个客户端同意。如果你的服务器包装了一个第三方API,实现你自己的同意屏幕。不要依赖第三方的同意cookie。
- 令牌有效期。短生命周期的访问令牌并带有刷新机制。被盗的1小时令牌是糟糕的。被盗的30天令牌是灾难性的。
- OAuth发现期间的SSRF保护。阻止私有IP,强制使用HTTPS,验证重定向。这是一个客户端问题,但构建SDK的服务器开发人员需要将其内置。
- 从不记录令牌。听起来显而易见。但令人惊讶地经常被违反。清除
Authorization头、查询字符串和结构化日志。
实现概览
规范提供了使用TypeScript、Python和C#的参考实现。TypeScript和Python SDK都使用Keycloak进行令牌内省。C# SDK使用ASP.NET Core内置的JWT验证,带有Keycloak权威URL。
所有三个都验证受众。所有三个都实现PRM发现流程。Python SDK的MCPServer类自动处理大部分样板代码——它发布PRM文档,返回带有正确头部的401状态码,并将令牌传递给你的验证器。
如果你从头开始,使用Python SDK的MCPServer和自定义TokenVerifier可能是正确实现认证流程的最快路径。
最后总结
MCP的授权规范比大多数开发人员预期的要更加注重安全。混淆的副手缓解措施、SSRF保护以及范围最小化指导反映了现实世界的攻击模式,而不是理论上的担忧。
如果你运行一个涉及用户数据的MCP服务器,实现认证。如果你构建一个代理到第三方API,实现每个客户端的同意。无论你做什么,验证受众声明。
规范在那里。参考实现在那里。攻击已经记录下来。2026年推出一个未受保护的MCP服务器没有借口。