iT邦幫忙

1

用 AI 寫 MCP server 很快就會跑,但「會跑」不等於「安全」——多租戶隔離的五條 best practice

  • 分享至 

  • xImage
  •  

前陣子有人在我發在 dev.to 一篇文章底下留言,大意是這樣:他用 AI 幫忙寫了一個 MCP server,一次就跑起來、回傳的資料也全對,於是就上了。後來才發現,這個 server 的權限範圍根本沒做——任何一個 token 都能讀到不屬於它的資料。他說了一句我覺得很值得記下來的話:

「它通過了我丟給它的每一個測試,因為我從頭到尾都只用我自己的身分在測。」

這不是他個人粗心。這是現在寫 MCP server 的預設狀態

為什麼「會跑」會騙過你

MCP server 現在非常好生。你把工具的輸入輸出描述清楚,AI 幫你把 handler 寫出來,接上 Claude 或 ChatGPT,說一句「幫我查這個」就有結果。第一次跑通的成就感很真實。

問題是:一旦你的 MCP server 能讀資料,它就是一個 data API。 而 data API 最基本的一件事,是回答「這個呼叫者,有沒有資格看這一筆」。這件事 AI 通常不會主動幫你補,因為你的 prompt 裡沒說;而你的測試也照樣會過,因為你手上只有一把 token、只會看到自己的資料。你看到對的結果,就以為對了。

這在資安上有個名字叫 confused deputy(被搞糊塗的代理人):一個有權限的程式,被誘導去替沒權限的人做事。MCP server 天生就是這種代理人——它拿著能存取整個資料庫的憑證,替對話另一端的使用者跑查詢。它有沒有把「查詢」限縮在「這個使用者該看的範圍」,就是安全與不安全的分界。

我在維護一個叫 toui.io 的短網址服務,前陣子幫它加了 MCP server,讓使用者可以在 AI 對話裡直接建短網址、查點擊成效。它是多租戶的——每個團隊只能看到自己的連結。下面五條,是我在把它弄對的過程中,覺得最值得先講給正要動手的人聽的。

一、別讓呼叫者把 id 傳進來

最直覺的工具設計,是讓 AI 傳一個 id 進來,例如 get_stats(campaign_id: 42)。這很危險——因為 42 是誰的,完全由你後端記得去檢查。漏一個地方沒檢查,別人的 42 就被讀走了。

換個做法:工具的輸入不收 id,改收名字或代號,由後端在這個使用者的範圍內自己解析。 我的工具收的是活動的「名稱」或短網址的「短碼」,後端拿到之後,只在這個團隊的資料裡去找對應的那筆。找得到就回、找不到就當作不存在。

一句話記住它:你接受的 id,就是你必須記得去授權的 id。在範圍內解析出來的名字,不可能變成被搞糊塗的代理人。

二、每個查詢都帶上租戶條件,而且回「查無此物」

就算你決定收 id,真正的防線也不在「收不收」,而在每一個 SQL 查詢有沒有把租戶條件帶上

-- 危險:先撈出來,再回頭檢查是誰的(很容易忘記第二步)
SELECT * FROM campaigns WHERE id = ?;

-- 安全:租戶條件直接寫進查詢,撈不到就是撈不到
SELECT * FROM campaigns WHERE id = ? AND team_id = ?;

差別看起來很小,但意義完全不同。第一種是「先拿到資料、再判斷能不能給」,只要哪個 handler 忘了第二步,就漏了。第二種是資料庫層級就撈不到別人的東西,你想漏都難。

還有一個容易忽略的細節:當別人的 id 撈不到時,回「查無此物」,不要回「你沒有權限(403)」。因為 403 等於告訴對方「這筆存在,只是不給你看」——他可以拿這個訊號去枚舉哪些 id 是有效的。回「查無此物」,他連「存不存在」都問不出來。

三、身分綁在 token 上,不綁在請求參數上

承上,那個 team_id 要從哪裡來?

只有一個合法來源:呼叫者的 token。 絕對不能從工具的參數裡讀。想像一下,如果 team_id 是 AI 傳進來的參數,那任何人只要改一個數字就能切換身分——這等於沒鎖。

在我的實作裡,每個請求的租戶身分一律從 token 解析出來,放進後端的 context,工具邏輯只從 context 拿,從來不碰請求的參數。工具收什麼參數是一回事,「這個請求算誰的」是另一回事,這兩者要徹底分開。

四、長命的 token 要有「即時撤銷」

這一條是我覺得最不明顯、也最想提醒大家的。

有些 token 是短命的、每次都會重新驗證;但 OAuth 那種授權通常是長命的——使用者授權一次,之後很長一段時間都拿著同一張 token。這帶來一個問題:如果這個使用者被停權了,或被踢出團隊了,他手上那張格式完全正確、還沒過期的 token,會不會還能用?

如果你只在發 token 的時候檢查一次身分,那答案是「會」——他會繼續讀到資料,資料還是對的形狀,只是對應著一個早就不該存在的身分。

正確做法是:每一個請求都重新確認這個使用者現在還是不是有效成員、帳號是不是還正常,不要相信 token 發出去那一刻的狀態。撤銷要即時生效,不能等 token 自然過期。

這裡有個結構性的教訓:我的系統有兩條認證路徑,一條(短命 token)因為每次都會去資料庫重驗,這個「即時撤銷」是免費附贈的;另一條(長命授權)什麼都沒繼承——同一個檢查,在一條路徑上自動生效、在另一條路徑上悄悄地不存在。這跟開頭那位留言者的處境是同一種病:你只驗過其中一條路徑,就默默假設另一條會繼承它的保證。 不會的,你得為每一條路徑各自把它補上。

五、測試裡一定要有「第二個租戶」

回到最前面那句話:「因為我從頭到尾都只用我自己的身分在測。」

這是所有多租戶 bug 的溫床。只要你的測試環境裡只有一個使用者、一個團隊,那所有「跨租戶隔離」的漏洞都不會被觸發——因為根本沒有「另一個租戶」讓你去越界。

解法便宜到有點不好意思:在測試資料裡種第二個租戶,讓他擁有一些資料,然後寫幾條測試,專門用第一個租戶的身分去存取第二個租戶的東西,並且斷言「應該查不到」。

我現在有兩條測試,存在的唯一目的就是伸手越過租戶邊界、然後被告知「這東西不存在」。它們如果早點存在,開頭那位留言者的 bug,第一次跑測試就會被抓到。

收尾

這五條其實是同一件事的五個面向:把「誰在呼叫」和「他能看到什麼」焊死在後端,不相信任何從請求裡送進來的東西。 id 不從參數收、租戶條件寫進每個查詢、身分只認 token、撤銷每次重驗、測試裡永遠有另一個人。

AI 幫你把 MCP server 寫到「會跑」很快。但「會跑」只證明了 happy path 通了,證明不了邊界守住了。在你把一個能讀真實資料的 MCP server 接上去之前,值得停下來,一條一條問自己:換成別人的身分來呼叫,這裡會發生什麼事?


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言