夜雨聆风学习资料网

ARTICLE · 1097910

你的AI助手正在把登录凭证,亲手交给攻击者

你的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等公开资料。

相关学习资料