你很可能也這么干過。給大模型配工具的時候,順手在描述里寫一句:“這個工具僅供只讀,不要修改任何東西。”然后放心地覺得,有這行字兜底,模型總該聽話。開發者 Chris 就是這么起的頭。他第一個接給 LLM 的工具,是讓他能從 Redis 里看一眼自己跑的作業隊列。下一步,他想釘死一件事:除了“看”,模型什么都干不了——不能入隊、不能刪除、不能重試。
最本能的招數就是把這要求寫進工具描述,或者塞進系統提示詞里。Chris 做了,然后盯著那段話琢磨了一分鐘,發現它什么也擋不住。調這個工具的是個語言模型。它讀到“不要修改”這句話的方式,跟讀你給它的每一條其他指令一樣:當成一個想說就說、想忽略就忽略的建議。一次多步推理的混亂鏈、一個被投毒的任務載荷、一個比今天更愛“動手”的未來模型,都足以讓那句描述變成擺設。描述就是文檔,它不構成權限檢查。
![]()
于是 Chris 不再試圖跟模型好好商量,而是直接讓“寫”這條通路在結構上壓根不存在。如果一種能力沒有接線,那就沒有任何提示詞、注入攻擊或者越獄手段能把它揪出來。真正起作用的不在提示層面,而在工具注冊的那一刻。
要理解這個防線究竟落在哪,得稍微回顧一下 MCP 工具的運行機制。一個 MCP 服務器對外暴露工具,客戶端先調用 tools/list 拉取名字和輸入模式。當模型想要行動時,主機會發送 tools/call,帶上工具名和參數,而你的服務器再把這個請求路由到你為那個名字注冊的處理器。由此自然得出兩件事:第一,廣告過的可用表面就是你注冊的那一套,多一個都沒有,模型只可能朝著它知道存在的工具去伸手;第二,調度本質上是個表查找。一個沒有處理器對應的名字就是錯誤,不是動作。萬一模型憑空捏造一個 delete_job,或者有份惡意負載唆使它去調用 delete_job,這個請求的歸宿就是“未知工具”,隨即死在當場。
這意味著防線不在于 Chris 一開始試圖部署的描述字句,而在注冊表和它底下那層處理器所使用的憑證。把邊界挪到這兩個位置之后,提示層就不再是安全考量了。
實際做法直截了當。這個隊列查看工具在注冊時一共引入四個讀接口:list_queues、queue_stats、list_jobs 和 get_job。而負責變更隊列的兩個工具——retry_job 和 delete_job——只在服務器沒有以“只讀模式”啟動的時候才會被注冊。加上 --read-only 參數運行,那兩行注冊調用永遠不執行,于是這個進程對外宣稱的工具就止步于四個讀接口,再無其他。代碼上看起來就是這樣:一個 McpServer 實例,先把讀表面掛上;寫表面則躲在只讀開關后面。開關一撥,retry_job 和 delete_job 就從進程的工具表里徹底消失——不是說每次調用才去禁用,而是壓根沒現身過。
走到這一步,安全感的來源就不一樣了。不再靠對模型講道理,而是讓它能觸達的動作集從根本上就不包含那些危險的寫路徑。攻擊者就算在某個載荷里寫滿了“快去調用 delete_job”,也只會看到一個 unknown tool 的錯誤。這不但在今天有效,當模型變得更主動、更愛在工具間跳轉時,這種設計反而會更牢靠。
Chris 把原理說得很清楚以后,很容易讓人聯想到更大的場景。凡是依賴大模型調用工具的場合,遲早會碰到“某些操作只想給一部分權限”的需求。在注冊表這一關上做文章,比在提示詞里打補丁,要穩健得多。架構里本來就有一層分派器,不如認真用好它。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.