SDK 信任了伺服器:MCP Python OAuth 漏洞如何偷走真實登入憑證
官方 MCP Python SDK 的高嚴重度漏洞,讓任何惡意工具伺服器只要回一個 404,就能擷取 OAuth client secret、授權碼與 PKCE 金鑰——修補版本為 1.30.0 與 2.2.0。
9 月 29 日,官方 MCP Python SDK 的維護者發布了一份措辭罕見直白的安全公告:惡意的 MCP 伺服器可以誘騙基於該 SDK 打造的應用程式,交出它用來登入真實服務的 OAuth 憑證。而且不是釣魚頁面上的假登入——是如假包換的真登入頁——與此同時,client secret、授權碼和 PKCE 證明金鑰悄悄流向攻擊者控制的 token 端點。
這個由資安公司 Cycode 發現並通報的漏洞,在兩個無人值守的 provider 上評為 High(7.5),互動式 provider 則為 6.5。修補已隨 mcp 套件的 1.30.0 與 2.2.0 版本發布。截至 9 月 29 日尚未指派 CVE 編號。
對一個在 2026 年透過 Model Context Protocol 把 AI agent 接上各種外部工具的生態系來說,這種漏洞值得慢讀而非瀏覽標題——因為它的機理解釋了 agent 安全為何如此困難。
MCP SDK 原本該做什麼
Model Context Protocol 是由 Anthropic 提出、現由 Linux Foundation 旗下 Agentic AI Foundation 維護的開放標準,用來把 AI 助理連接到外部工具與資料源。當 MCP 客戶端連上一個要求登入的伺服器時,SDK 必須回答兩個問題:要把使用者送去哪裡登入?換到的授權碼要拿到哪個端點換取 token?
兩個答案都是 URL,而答對是安全屬性。憑證送錯地方,就是被竊。
SDK 透過一個叫 discovery 的流程解析這些 URL。安全路徑是:客戶端先問 MCP 伺服器「你的登入由誰負責」,拿到指向授權伺服器(Google、Okta、Azure AD 等)的 URL,抓取該 provider 的設定檔,然後驗證設定檔中的 issuer 欄位——provider 的自我身分宣告——是否與第一步拿到的 URL 相符。這個 issuer 檢查,就是用來抓伺服器謊報登入 provider 的關鍵防線。
另外還有一條備援路徑:如果現代 discovery 請求失敗——伺服器回傳 404——SDK 就退而直接向 MCP 伺服器本身索取登入設定。
一個 404,解除所有防線
備援路徑正是全面潰堤之處。走這條路時,SDK 從未拿到可比對的 URL,內部值是 None。issuer 驗證程式碼是這樣寫的:
if self.context.auth_server_url is not None:
validate_metadata_issuer(asm, self.context.auth_server_url)
白話就是:「如果有拿到 URL,就用它驗證 provider 身分。」但攻擊者能決定這個 URL 是否存在。只要對 discovery 請求回一個 404——基本上什麼都不用做——SDK 就落入備援路徑,檢查不是沒通過,而是根本沒執行。SDK 接著全盤接受惡意伺服器提供的登入設定,不做任何驗證。
第二道防線同時失效。SDK 會為儲存的憑證貼上所屬登入 provider 的標籤,使用前先核對。但它核對的是設定檔裡的 issuer 欄位——也就是第一道檢查沒跑、由攻擊者掌控的那個欄位。攻擊者只要把 issuer 設成受害者真實登入 provider 的名字,憑證綁定檢查就用一個謊言通過了。你真正的憑證被保留了、被使用了,只是被送去攻擊者的端點。
登入頁是真的——這正是攻擊成立的原因
讓這個漏洞從理論變成實戰的關鍵,在於使用者唯一會仔細看的那一眼,看到的是真正的登入頁。攻擊者的設定把登入 URL 指向真實 provider。使用者被導向真正的 Google、真正的 Okta、真正的 Azure AD——真 URL、真憑證。頁面沒有任何問題,因為它本來就是正版。
使用者按同意後,授權碼回到客戶端,SDK 把它連同 client secret 與 PKCE 證明金鑰——那個專門設計來防止授權碼被盜用的一次性秘密——打包送往它以為是 provider token 端點的地方。但那個 URL 來自攻擊者的設定。Cycode 在強制 PKCE 的真實授權伺服器上端到端示範了完整攻擊鏈:攻擊者用竊得的組合換到真正的存取權杖。
在受害者這端,MCP 伺服器只是「登入後就卡住了」。伺服器不穩嘛,關掉分頁繼續做事。
Cycode 用三組配置的實驗孤立出關鍵控制:攻擊配置(404 discovery)下憑證被竊;對照組唯一差別是伺服器有回應 discovery,issuer 檢查執行、不符被抓到、流程在任何東西離開機器前终止。帳號被接管與安全的全部差別,就在那一次檢查有沒有跑。
誰受影響、該怎麼辦
受影響:以 MCP Python SDK 作為 HTTP 客戶端、使用 OAuthClientProvider、ClientCredentialsOAuthProvider 或 PrivateKeyJWTOAuthProvider(含已棄用的 1.x RFC7523OAuthClientProvider),且可能在持有真實登入 provider 憑證的情況下連到自己無法完全掌控的伺服器的應用程式。版本範圍為 1.9.1–1.29.1 與 2.0.0–2.1.1。不受影響:用 SDK 打造的 MCP 伺服器本體、本地 stdio 客戶端、自行附加 token 的客戶端。
修法:升級到 mcp 2.2.0(2.x 線)或 1.30.0(1.x 線)。修補後的 SDK 會在抓取任何設定之前先確定預期的登入 provider,拒絕指向其他 provider 的設定,並為儲存的憑證加上標籤,確保屬於某個 provider 的憑證永遠不會被送往另一個。
但光升級不是全部的修法。公告列出三個後續動作:
- 明確傳入
issuer=:若使用ClientCredentialsOAuthProvider或PrivateKeyJWTOAuthProvider,不傳的話這些 provider 仍會跟隨 MCP 伺服器指定的任何登入 provider。1.30.0 上的警告只是標準 Python 棄用警告——預設隱藏、極易錯過;3.0 起將成為必要參數。 - 清除已儲存的註冊資訊:舊版儲存的註冊沒有 provider 標籤,仍是未綁定狀態。升級後清一次儲存的 OAuth client 資訊,讓客戶端帶著標籤重新註冊。
- 若可能已暴露則輪換:若客戶端在升級前曾連過不可信任的伺服器,請至登入 provider 輪換 client secret 並撤銷 token。client secret 是長期性憑證、常常永不過期;secret 本身沒換掉的話,輪換授權碼或撤銷現有 token 都無濟於事。
為什麼這個「模式」比 CVE 更重要
把鏡頭拉遠,這個漏洞是一個通用模板:某個安全檢查只在特定資料存在時執行,而攻擊者能決定那份資料存不存在。更糟的是,被跳過的值不是沒被驗證而已——它還會被當成受信任的輸入流入下一道安全檢查,並主動滿足它。issuer 驗證存在、憑證綁定存在、audience 限制也存在。它們全被餵了同一份未驗證的輸入,然後全都說「看起來沒問題」。
嚴重度評分也低估了實務風險。互動式 provider 的 6.5 分假設使用者明知故犯地連上惡意伺服器。但註冊表投毒(MCP 目錄裡的拼字仿冒條目)、透過 prompt injection 操縱自行挑選伺服器的 agent、或對合法伺服器名稱的 DNS 劫持——每一種都讓「使用者自己選的」這個假設完全消失。而對機器對機器的 provider 來說,本來就沒有人類在迴路裡。
MCP 已成為 agent 生態系的連接組織,下載量以億計。這個漏洞提醒我們:在那個世界裡,你的 agent 對話的工具伺服器不只是一項能力——它是你身分驗證流程的參與者。「少信任它一點」現在是升級必做事項,而不是一種姿態。