ARTICLE · 1097910
你的AI助手正在把登录凭证,亲手交给攻击者
昨天一个做安全的朋友在群里发了一条消息:“MCP Python SDK出事了,你们谁在用赶紧看。”
我问他严重吗。他回了一句:“你的AI客户端拿着登录凭证,去敲了攻击者家的门。”
这句话让我想了很久。
问题出在哪?
MCP(Model Context Protocol)是Anthropic推出的一个开放标准,让AI应用能连接外部工具和数据。而mcp这个Python包,是它的官方SDK——用来构建MCP服务器和客户端的标准工具。
这次的问题,出在OAuth登录流程里。
当MCP客户端需要登录一个服务时,它会先问它要连接的服务器:“你的登录服务(授权服务器)在哪里?”
在受影响的版本里,SDK没有好好检查这个回答。
一个恶意的MCP服务器可以告诉客户端:“我的登录服务在attacker.com。”客户端信了,然后把自己的客户端密钥(client secret)、授权码(authorization code),以及PKCE证明密钥(proof key),全部发到了attacker.com。
攻击者拿到这些东西,就能去真正的登录服务那里,换取一个有效的访问令牌。
而这个令牌,带着你的应用被授予的所有权限。
三个东西被送出去了,一个比一个关键
先说一下这三个凭证分别是什么。
客户端密钥:这是应用在登录服务那里注册时拿到的“身份证”。它是长期有效的——除非你主动去换,否则它一直能用。
授权码:登录流程中,服务返回给你的一次性代码,用来换取最终的访问令牌。
PKCE证明密钥:这是用来防止授权码被中途截获后重放的。设计初衷是让攻击者即使偷到了授权码,也没法用——因为他不知道证明密钥。
但这次,三个一起送出去了。
授权码有了,证明密钥有了,客户端密钥也有了。攻击者拿着这三样东西,去真正的登录服务那里完成完整的令牌交换流程。登录服务看到的是一个合法的、完整的、带着有效PKCE证明的请求。它没有任何理由拒绝。
Cycode的研究人员做了完整的演示测试,确认了这个攻击链是成立的。而且换来的令牌,带着你的应用被授予的完整权限。
最让人不安的部分:交互式登录看起来一切正常
这个漏洞影响两种OAuth提供者:交互式(需要有人点登录)和机器对机器(不需要人参与)。
机器对机器的那两个,不需要任何人参与。攻击者只需要让你的应用连上他的恶意MCP服务器,剩下的流程自动完成。CVSS评分7.5。
交互式的那个,评分6.5,因为“需要有人批准登录”。Cycode的研究人员说了一句话,我觉得是整件事里最让人后背发凉的:
用户批准的那个页面,是真实的登录页面。
所以用户看到的是:一个正规的登录界面,一个“同意”按钮,一个看似正常的授权流程。没有任何迹象表明,这个流程的终点不是他们以为的那个服务。
用户以为自己是在授权一个正常的工具。实际上,他们授权出去的凭证,正在被送往攻击者的服务器。
谁受影响?
受影响的条件很具体:
你使用SDK作为MCP客户端,通过HTTP连接,并且使用了以下OAuth提供者之一:
OAuthClientProvider
ClientCredentialsOAuthProvider
PrivateKeyJWTOAuthProvider
已弃用的1.x版本中的RFC7523OAuthClientProvider
同时,你的应用连接了一个你无法完全控制的MCP服务器,并且持有真实登录服务的凭证。
不受影响的包括:用SDK构建的MCP服务器、本地(stdio)客户端、以及自己附带令牌的客户端。
修复方案:升级,但不止是升级
修复版本很明确:1.x线升级到1.30.0,2.x线升级到2.2.0。
在修复版本里,客户端会先确定它期望的登录服务是哪一个,然后拒绝任何指向不同服务的元数据。逻辑上,这是正确的修复方向——不要盲目相信服务器告诉你的授权服务器地址,而是自己先搞清楚应该信任谁。
但升级本身,对其中两个提供者来说,不够。
如果你用的是ClientCredentialsOAuthProvider或PrivateKeyJWTOAuthProvider,安全公告里有一句话写得很直接:“除非你也传入issuer=来指定这些凭证所属的登录服务,否则升级不改变任何事。”
不传issuer=,它们仍然会跟着MCP服务器指向的任何地方走。
而更麻烦的是,在1.30.0上,关于这个的警告是一个标准的弃用警告(DeprecationWarning)——Python默认会隐藏它。所以即使你升级了,如果没有显式传入issuer=,你的应用可能仍然是脆弱的,而你不会看到任何警告。
那个已弃用的RFC7523OAuthClientProvider,根本没有issuer=这个选项。唯一的出路是迁移到其他两个提供者之一。
还有一个容易被忽略的步骤:升级之后,需要清除一次所有已存储的OAuth客户端注册。因为旧的注册没有和登录服务绑定,它们会一直保持“未绑定”的状态。如果客户端之前可能连接过不受信任的服务器,还应该在登录服务那里轮换客户端密钥并撤销令牌。
对于更早的版本,安全公告说得很坦白:除了只连接你信任的MCP服务器之外,没有其他变通方案。
一个时间线上的细节
修复的issuer检查是在9月7日随1.30.0和2.2.0一起发布的。
但它们在发布说明里,被列在了“行为变更”(Behaviour changes)下面,而不是“安全修复”。安全公告是9月28日才发布的——同一天,Cycode公开了他们的分析。
也就是说,从修复上线到安全公告发布,中间隔了三周。而在这三周里,升级了版本但不知道issuer=这件事的用户,可能以为自己已经安全了。
安全公告里特别提到,目前没有发现任何使用这个漏洞的攻击。这是一个好消息。但漏洞已经存在,PoC已经公开,攻击者拿到工具只是时间问题。
如果你在用MCP Python SDK
第一,确认版本。 运行pip show mcp,看看你的版本号。如果低于1.30.0(1.x线)或2.2.0(2.x线),立即升级。
第二,如果你用ClientCredentialsOAuthProvider或PrivateKeyJWTOAuthProvider,确认传入了issuer=。 不要依赖默认行为。显式指定你的登录服务的issuer URL。
第三,清除旧的OAuth客户端注册。 升级后,清一次存储的注册记录,让新的注册和登录服务绑定。
第四,如果你之前可能连接过不受信任的MCP服务器,轮换客户端密钥并撤销令牌。 这不是一个“以防万一”的建议。如果客户端曾经连过一个恶意的MCP服务器,凭证可能已经在外面了。
第五,如果你用RFC7523OAuthClientProvider,迁移。 这个提供者在1.x里没有任何issuer保护,而且已被弃用。
一点想法
MCP的定位是“让AI连接一切”。这个愿景很好,但“连接一切”的另一面是“信任一切”。
OAuth的设计前提是:客户端知道自己在跟谁说话。 但当MCP服务器负责告诉客户端“你应该去哪里登录”时,这个前提就变得脆弱了。攻击者不需要破解任何加密,不需要利用任何内存漏洞。他只需要在元数据里写一个不同的地址,然后等着客户端把凭证送过来。
你让AI去帮你操作一个外部工具。它乖乖去登录了。但它登录的那个“服务”,根本不是它以为的那个。
这不是MCP独有的问题。任何“客户端从服务器获取授权服务器地址”的设计,都存在同样的风险。但MCP生态的快速扩张——大量第三方MCP服务器、大量不同的OAuth提供者——让这个风险的暴露面变得非常大。
修复已经发布了。但修复的质量,取决于你有没有正确地使用它。升级版本只是第一步。传入issuer=,清除旧注册,轮换凭证——这些步骤,一个都不能少。
本文信息综合整理自The Hacker News、MCP Python SDK安全公告、Cycode研究报告、modelcontextprotocol/python-sdk 1.30.0发布说明及VulDB等公开资料。