加固 MCP stdio 伺服器
Endurecer servidores mcp stdio
Langflow 的 CVE-2026-105740(CVSS 9.9)允許任何已登入用戶登記由 bash -c 執行的 MCP stdio 命令。文章建議只讓用戶選擇伺服器 ID,將 ID 對應至經審核及固定版本的 argv,停用 shell,並以允許清單建立子程序環境。
文章從 Langflow 命令執行漏洞延伸至本機設定、團隊允許清單和雲端角色權限,呈現最小權限原則如何落實在不同部署層。
使用 stdio 傳輸的 MCP 伺服器,就是 Agent 以你的身分執行的一條命令列指令。到了 2026 年 10 月,這不再只是抽象概念:Langflow 的 CVE-2026-105740(CVSS 9.9)讓任何已驗證用戶都能註冊一個 stdio 伺服器,其指令會直接執行 bash -c。這篇文章會逐一説明這個漏洞、真正可靠的修補方案,以及對一部執行 Claude Code 的開發機所作的審核;審核發現一行 npx,指向一個無人持有的 npm scope。
起點:Langflow 做了甚麼
安全公告(GHSA-7w94-79vh-5mr2)列出了相關程式碼行。API 允許任何已驗證用戶提交包含 command、args、env、url 和 headers 的 MCPServerConfig。沒有「任何允許執行的指令清單,也沒有任何危險指令封鎖清單」。之後,指令會以下列方式啟動:
args=["-c", f"exec {command_str} || echo 'Command failed...'"]
用戶的 env 字典會合併至子程序環境。觸發點不是「執行這個伺服器」,而是 GET /api/v2/mcp/servers?action_count=true,也就是列出伺服器的請求。根據安全公告,受影響版本是 1.8.3,並在 1.9.0 修補;分流備註亦列出 1.10.3,因此 1.10.3 或更新版本涵蓋兩種解讀。蜜罐回報的漏洞利用嘗試有數千次,屬低千位數;其中一份附有方法説明的報告,記錄自 9 月 19 日起來自單一感測器的 2,023 次嘗試。
程式碼伴讀:MCP stdio 伺服器是一條 Shell 指令
| 資料夾 | 貼文區段 | 內容介紹 |
|---|---|---|
00-the-bug/ |
第 0 回合:漏洞 |
vulnerable_spawn.py:將 CVE-2026-105740 的模式簡化成兩行,並使用無害的酬載。刻意將它保留為死路。 |
01-safe-spawn/ |
第 1 回合:修補 |
safe_spawn.py:伺服器 ID 對應至固定的 argv,不使用 shell,環境變數來自允許清單。 |
02-audit-your-machine/ |
第 2 回合:評估 |
mcp_audit.py:盤點 Agent 將啟動的每個 MCP 伺服器,並標記 STDIO / UNPINNED / AUTO-YES / NOT-FOUND / ENV / PLAIN-HTTP。 |
03-managed-policy/ |
第 3 回合:機羣政策 | Claude Code 受管理設定:精確的 serverCommand 允許清單,並鎖定於受管理範圍。 |
04-aws-tool-role/ |
第 4 回合:身分 | 在 AWS 上執行的工具所用的 IAM 角色:單一日誌羣組、明確拒絕規則,以及可防範混淆代理人問題的信任政策。 |
05-checklist/ |
第 5 回合 | 今天要在你的機器上檢查甚麼,以及平台必須強制執行甚麼。 |
建議閲讀次序
先執行 02;只需十秒,而且……
第 0 回合:簡化版漏洞
00-the-bug/vulnerable_spawn.py只保留兩行關鍵程式碼,並執行無害的酬載:
1: mcp-server-started
2: mcp-server-started
3: mcp-server-started substitution-ran-before-exec
4: not node: PATH from the env field won
三個不太明顯的細節:
-
第 2 行看似是一種緩解措施,但其實不是。
exec會以第一個程式取代 shell,因此; echo chained永遠不會執行。命令替換則仍會執行,因為 shell 會在exec之前展開$(...)(第 3 行)。 -
env 欄位本身就是一種注入方式。 第 4 行要求
node --version,卻取得另一個二進制檔,因為PATH來自請求。LD_PRELOAD對 Linux 上任何動態連結的程式也有同樣效果。 -
不過,這些都不是最重要的。 command 欄位接受任何程式。即使解析器完美地拒絕
$(...),只要用戶將curl ... | sh輸入為指令,它仍會執行。漏洞在於,用戶可以決定執行甚麼,而不是字串如何解析。
第 1 回合:經得起考驗的修補方案
01-safe-spawn/safe_spawn.py:
REGISTRY: dict[str, ServerSpec] = {
"playwright": ServerSpec(argv=("/usr/local/bin/playwright-mcp", "--headless")),
"echo-demo": ServerSpec(argv=("/bin/echo", "mcp-server-started")),
}
def start_stdio_server(server_id: str, user_args: list[str] | None = None) -> str:
spec = REGISTRY.get(server_id)
if spec is None:
raise NotAllowed(f"unknown MCP server id: {server_id!r}")
if user_args:
raise NotAllowed("arguments are part of the reviewed spec, not the request")
...
out = subprocess.run(list(spec.argv), env=child_env(spec), shell=False, ...)
1: mcp-server-started
refused: unknown MCP server id: "bash -c 'echo pwned'"
refused: arguments are part of the reviewed spec, not the request
refused: binary not installed: /usr/local/bin/playwright-mcp
以下規則按效益由高至低排列:
- 用戶只選擇 ID,絕不傳送指令。
- 每個 ID 都對應一組精確的 argv,並像依賴項般審核;固定版本會在映像檔建置時安裝。啟動時不會下載任何內容。
- 不使用 shell。
- 子程序環境由允許清單(
HOME、LANG、TZ、固定的PATH)以及管理員設定的值組成。呼叫端的環境變數絕不會與請求資料合併。 - 新增 ID 屬於管理員變更,並會留下審計記錄。在 Langflow 中,設定一直儲存在專案設定內,而 flow 修訂不會檢查該處。
第 2 輪:檢查你自己的電腦
即使沒有存在漏洞的伺服器,同樣的風險依然存在。程式碼代理會讀取
設定檔中的 stdio 伺服器定義,並以你的身分啟動它們。
02-audit-your-machine/mcp_audit.py 會列出常見位置中的每個伺服器
(~/.claude.json 的 user 和 local scopes、專案的 .mcp.json、
Kiro、Cursor、Claude Desktop),並標記找到的項目。它只讀取這些資料,此外每個套件只會向 npm 或 PyPI registry 發出一次 HEAD 請求。
在我撰寫這篇文章所用的電腦上:
~/.claude.json (local scope: ~/projects)
playright npx @playwrigth/mcp@latest STDIO UNPINNED NOT-FOUND(npm:@playwrigth/mcp)
~/.claude.json (local scope: ~/projects/myapp)
playright npx @playwrigth/mcp@latest STDIO UNPINNED NOT-FOUND(npm:@playwrigth/mcp)
2 servers, 2 stdio, 2 with a finding beyond STDIO
playwrigth 是錯字,而這個 npm scope 並不存在。
$ curl -s -o /dev/null -w '%{http_code}\n' https://registry.npmjs.org/@playwrigth/mcp
404
$ curl -s https://registry.npmjs.org/-/org/playwrigth/package
{"error":"Scope not found"}
任何人都可以在 npm 建立組織 playwrigth,並發布 @playwrigth/mcp。
local scope 的伺服器會在工作階段於該目錄啟動時載入,而
@latest 代表下次啟動時會解析至最後發布的任何版本。伺服器一直無法運作,所以沒有人察覺:MCP 連線失敗只會在 /mcp 留下一行記錄,不會成為令工作停止的錯誤。
有兩個不太明顯的細節:
-
registry 查詢正是發現問題的關鍵。如果沒有這項查詢,這行設定看起來就像一般的 Playwright 伺服器。審核旗標
NOT-FOUND正是為這種情況而設:錯字、尚未有人註冊的 scope,或已取消發布的套件。 -
核准專案
.mcp.json並非完整答案。 Claude Code 在互動式工作階段中使用 project scope 伺服器前會先詢問,並會忽略複製而來的儲存庫自行提交的核準項目。在claude -p中,Agent SDK 工作階段及部分雲端工作階段會「不經詢問便載入 project scope 伺服器」。這個錯字所在的 local scope 屬於用戶自己的設定,因此完全沒有核準步驟。
修正方法是對每個受影響的目錄執行一條命令:
claude mcp remove playright
claude mcp add playwright -- npx -y @playwright/mcp@0.0.83
第 3 輪:為整個設備羣制定政策
開發人員可以修正錯字。團隊則需要制定政策,令錯字失效。Claude Code 會從
受管理設定(03-managed-policy/managed-settings.json)讀取 allowedMcpServers 和 deniedMcpServers:
{
"allowManagedMcpServersOnly": true,
"allowedMcpServers": [
{ "serverUrl": "https://api.githubcopilot.com/*" },
{ "serverUrl": "https://mcp.example.com/*" },
{ "serverCommand": ["npx", "-y", "@playwright/mcp@0.0.83"] }
],
"deniedMcpServers": [
{ "serverCommand": ["npx", "@playwright/mcp@latest"] },
{ "serverUrl": "http://*" }
]
}
只要存在 serverCommand 項目,每個 stdio 伺服器都必須與清單中的某個命令完全相符,而且每個引數都必須按次序排列。含有錯字的那行不符合條件,因此不會載入。
文件中有三個不太明顯的細節:
-
serverName並不是安全控制。它只是輸入在claude mcp add中的標籤。任何人都可以任意命名任何github伺服器。請使用serverCommand和serverUrl。 -
serverCommand不會比對env。文件直接指出:「部分環境變數會改變node在啟動時載入的內容。」這就是另一個產品中的 LangflowLD_PRELOAD問題。如果環境變數會影響結果,便應在managed-mcp.json中定義伺服器,管理員亦可在該處設定伺服器的環境變數。 -
沒有
allowManagedMcpServersOnly,用戶便可以擴大清單。允許清單會合併所有 scope 的設定,包括~/.claude/settings.json。拒絕清單一律會合併,而拒絕規則總是優先。
第 4 輪:工具執行者的身分
在筆記型電腦上,stdio 伺服器會繼承開發人員擁有的一切:
~/.aws、鑰匙圈、SSH agent、shell 歷史記錄。在平台上,同一個
伺服器則應該是具有自身身分的遠端服務。這正是採用遠端傳輸方式最有力的理由:HTTP 伺服器會在自己的程序中執行,使用自己的角色,並由可撤銷的驗證機制保護,無須接觸任何人的筆記型電腦。
04-aws-tool-role/ 就是 AWS 對此概念的做法:
- 讀取一個日誌羣組、讀取指標,僅此而已。
-
明確指定
Deny、iam:*、sts:AssumeRole、secretsmanager:GetSecretValue、ssm:GetParameter*及kms:Decrypt,這樣之後加入的寬泛Allow就 無法擴大範圍。 - 信任政策只允許這個帳戶中的 ECS 任務擔任該角色
(
aws:SourceAccount+aws:SourceArn)。
一個不易察覺的細節:cloudwatch:GetMetricData 沒有資源層級的範圍限制。如果
指標名稱屬於敏感資料,解決方法是使用獨立帳戶,而不是寫出更巧妙的
statement。
這個角色無法解決的是對外流量。能呼叫任何 URL 的唯讀工具,可以 把讀取到的資料傳送到任何地方。應在路由只通往所需 AWS 端點的環境中執行它。
良好的預設設定是怎樣的
Azure MCP Server 3.0 在同一週轉為穩定版。它是雲端無關的預設設定參考,值得 參考:
--read-only模式,亦會排除發送電郵及 SMS 的工具。- 使用
--tool篩選,並「完全比對公開的名稱,不區分大小寫」。 - 防止 SSRF 的端點驗證擴展至更多資料平面及 控制平面端點。
- PostgreSQL 和 MySQL 查詢工具現在標記為 「具破壞性、非冪等,且非唯讀」,而 SQL 動詞篩選已移除。指引建議使用 最低權限的資料庫權限,而不是嘗試篩選 SQL 文字。
最後一點從另一個角度印證了第 0 輪的教訓:不要試圖
透過解析輸入,令強大的原語變得安全。應賦予它一個無法
執行危險操作的身份。
教訓
- 閲讀其他人的 CVE 前,先量度自己的設定。 稽核 發現,撰寫本文的機器上有一個可被他人認領的套件。
- stdio 伺服器的定義就是程式碼。 檢查它、固定版本,並決定誰 可以寫入它,做法應與處理 CI 工作流程一樣。
- 不要試圖靠解析輸入來防止命令執行。 移除用戶 選擇命令的能力。
-
環境變數是輸入資料。
PATH、LD_PRELOAD和NODE_OPTIONS與命令本身一樣,決定實際執行的內容。 - 靜默失效就是攻擊面。 一個數月來從未連線的伺服器, 就是一個無人留意的名稱。
- 身份與對外流量才是真正的邊界。 唯讀旗標和工具篩選有幫助; 無法執行危險操作的角色才經得起考驗。
來源:DEV Community · dev.to