跳到正文
DEV Community· Franchesco Romero·· 2 小時前精選AI 評分78

加固 MCP stdio 伺服器

Endurecer servidores mcp stdio

AI 導讀

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

三個不太明顯的細節:

  1. 第 2 行看似是一種緩解措施,但其實不是。 exec會以第一個程式取代 shell,因此 ; echo chained 永遠不會執行。命令替換則仍會執行,因為 shell 會在 exec 之前展開 $(...)(第 3 行)。
  2. env 欄位本身就是一種注入方式。 第 4 行要求 node --version,卻取得另一個二進制檔,因為 PATH 來自請求。LD_PRELOAD 對 Linux 上任何動態連結的程式也有同樣效果。
  3. 不過,這些都不是最重要的。 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

以下規則按效益由高至低排列:

  1. 用戶只選擇 ID,絕不傳送指令。
  2. 每個 ID 都對應一組精確的 argv,並像依賴項般審核;固定版本會在映像檔建置時安裝。啟動時不會下載任何內容。
  3. 不使用 shell。
  4. 子程序環境由允許清單(HOME、LANG、TZ、固定的 PATH)以及管理員設定的值組成。呼叫端的環境變數絕不會與請求資料合併。
  5. 新增 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 留下一行記錄,不會成為令工作停止的錯誤。

有兩個不太明顯的細節:

  1. registry 查詢正是發現問題的關鍵。如果沒有這項查詢,這行設定看起來就像一般的 Playwright 伺服器。審核旗標 NOT-FOUND 正是為這種情況而設:錯字、尚未有人註冊的 scope,或已取消發布的套件。
  2. 核准專案 .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 伺服器都必須與清單中的某個命令完全相符,而且每個引數都必須按次序排列。含有錯字的那行不符合條件,因此不會載入。

文件中有三個不太明顯的細節:

  1. serverName 並不是安全控制。它只是輸入在 claude mcp add 中的標籤。任何人都可以任意命名任何 github 伺服器。請使用 serverCommand 和 serverUrl。
  2. serverCommand 不會比對 env。文件直接指出:「部分環境變數會改變 node 在啟動時載入的內容。」這就是另一個產品中的 Langflow LD_PRELOAD 問題。如果環境變數會影響結果,便應在 managed-mcp.json 中定義伺服器,管理員亦可在該處設定伺服器的環境變數。
  3. 沒有 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 輪的教訓:不要試圖
透過解析輸入,令強大的原語變得安全。應賦予它一個無法
執行危險操作的身份。

教訓

  1. 閲讀其他人的 CVE 前,先量度自己的設定。 稽核 發現,撰寫本文的機器上有一個可被他人認領的套件。
  2. stdio 伺服器的定義就是程式碼。 檢查它、固定版本,並決定誰 可以寫入它,做法應與處理 CI 工作流程一樣。
  3. 不要試圖靠解析輸入來防止命令執行。 移除用戶 選擇命令的能力。
  4. 環境變數是輸入資料。 PATH、LD_PRELOAD 和 NODE_OPTIONS 與命令本身一樣,決定實際執行的內容。
  5. 靜默失效就是攻擊面。 一個數月來從未連線的伺服器, 就是一個無人留意的名稱。
  6. 身份與對外流量才是真正的邊界。 唯讀旗標和工具篩選有幫助; 無法執行危險操作的角色才經得起考驗。

來源:DEV Community · dev.to