MCP Inspector 2.15.0 一次修四個漏洞:SSRF 守衛的 IPv4 清單只列一個網段,POST 的事件串流沒設上限
一個專門連到不可信 server 的測試工具,它那道「別去碰雲端 metadata 位址」的守衛,底層到底是怎麼決定哪個位址不能碰?
我會問這題,是因為 2026-11-12 發的 MCP Inspector 2.15.0,release notes 開頭就是一句 “2.15.0 fixes four vulnerabilities.”。四個洞散在四個不同的地方,乍看是四件互不相干的事。拆開來讀,它們問的是同一個問題:防護的範圍,畫在哪裡。
先交代我讀了什麼:2.15.0 與 1.0.5 的 release notes、四則 GitHub security advisory,以及 2.15.0 tag 上 core/mcp/node/fetchGuard.ts 的幾個片段(整份 611 行,我沒有逐行讀完)。我沒有安裝任何版本,也沒有重現任何 PoC。下面出現的記憶體曲線、PWNED_BY_ENV 都是 advisory 作者自己的描述,不是我跑出來的。另外 GHSA-fvg8 與 GHSA-5572 這兩則的正文自填 severity 為 HIGH,和 GitHub metadata 欄位不一致;我原因不明,所以全文只引 metadata 的值。
守衛手上的名單,長什麼樣子
先拆最直接的那個。GHSA-fvg8-f358-w2c8 是 SSRF,metadata 欄位寫 severity 為 low、CVSS 3.1。advisory 的根因只有一句話:守衛的封鎖清單是寫死的,只有 169.254.0.0/16、它的 IPv4-mapped IPv6 形式、fe80::/10、fd00:ec2::254。原文接著說 “Other cloud metadata endpoints, notably Alibaba Cloud’s documented 100.100.100.200 metadata IP, are not blocked.”
觸發條件也寫得很具體:當 server 控制的 OAuth URL 是一個 IP literal,例如 http://100.100.100.200/latest/meta-data/...。這裡的「server 控制」值得停一下。Inspector 連的是你要測的那個 MCP server,它回傳什麼 OAuth 網址,Inspector 就會去抓;抓的人是你這台機器,不是 server。你在雲端主機上跑它的話,那個 server 就等於借你的手去敲 metadata 服務的門。
守衛本身是一個白紙黑字的清單。2.15.0 的原始碼裡,判斷那一行長這樣:
1 | return (a === 169 && b === 254) || METADATA_IPV4_ADDRESSES.has(ip); |
前半段是區間,只要前兩個八位元組是 169.254 就擋;後半段是 METADATA_IPV4_ADDRESSES 這個 Set,裡面有 100.100.100.200 和 192.0.0.192。上方註解寫它們分別是 Alibaba Cloud ECS 和 Oracle Cloud Infrastructure Classic 的 metadata 服務。這兩個位址是這次新加的,release notes 說連同它們的十進位與 IPv4-mapped IPv6 寫法一起擋,所以 /api/fetch 代理和 OAuth discovery 都不能再被指到那裡。
另外我讀到兩處旁證。MAX_FETCH_REDIRECTS = 5,迴圈裡每一跳都再查一次(約 L500);L193 附近的註解寫,名稱解析出來的結果只要含 metadata 位址,也視為惡意。換句話說,守衛不只看你給的網址,轉址和 DNS 結果也過同一道清單。
| 位址 | 判斷方式 |
|---|---|
169.254.0.0/16 |
整段區間 |
100.100.100.200(註解:Alibaba Cloud ECS) |
精確列舉,不擋所在區間 |
192.0.0.192(註解:Oracle Cloud Infrastructure Classic) |
精確列舉,不擋所在區間 |
為什麼不乾脆擋整個 100.64.0.0/10
一個自然的問法是:100.100.100.200 在 100.64.0.0/10 裡面,把整段擋掉不就好了?
原始碼註解把這個念頭擋回來了。L134-136 說這兩個位址是 “blocked exactly, never by their enclosing range”,理由是 100.100.100.200 落在 CGNAT 的 100.64.0.0/10,Tailscale 也用這一段,它的 MagicDNS resolver 是 100.100.100.100。1.0.5 的 release notes 講得更白:附近那一整段 “stays reachable, so Tailscale and other CGNAT targets still work.”
所以這是一個取捨,不是疏忽。區間寫得寬,會誤傷用 Tailscale 連內網 MCP server 的人;列舉寫得精確,就得靠有人記得每一朵雲的 metadata 位址。我的立場是精確列舉比擋整段好,因為一個測試工具如果連不上自己的 tailnet,使用者會直接把守衛關掉,那比漏一個位址更糟。但我也知道代價:每多一朵有自己 metadata 位址的雲,這份清單就欠一筆。
什麼會讓我改變想法?如果 fetchGuard 另外有一個我沒讀到的模式,例如讓使用者明確列出允許的目標、其餘預設拒絕,那這份黑名單的壓力就小很多。611 行我只讀了片段,這個我不能排除。
同一個修正也進了 v1 這條已棄用的線:1.0.5 是 “a security release for the deprecated v1 line”,升級指令是 npx @modelcontextprotocol/inspector@v1-latest。這則 advisory 的受影響範圍,v2 是 2.0.0 到 2.14.0,v1 是 1.0.4 以下,修在 1.0.5。
水桶沒有溢流孔:POST 的事件串流
讓我最意外的是第二個洞,所以多花點篇幅。
GHSA-9w48-x22j-rrm7,severity medium、CVSS 6.5,影響 2.0.0 到 2.14.0。攻擊者是上游那個 MCP server,advisory 原文說 “it needs no Inspector credentials.”。做法是:對一個 JSON-RPC 請求,用 text/event-stream 的 POST 回應,不送最終那則回覆,也不關串流。Inspector 的 web backend 就會把整條串流緩衝起來,沒有大小上限。
根因在 fetchTracking.ts 的 isLongLivedStreamResponse(method, contentType),advisory 引的那一行是:
1 | if (method.toUpperCase() !== "GET") return false; // POST+SSE => not long-lived |
這一行背後有個假設:長時間開著的事件串流是 GET 請求,POST 的事件串流應該很快就結束。可是 advisory 描述的攻擊,正是 server 用 POST 的回應開一條事件串流,然後不收尾。POST 的串流被歸進「不會久留」的那一邊,走「clone 之後讀到底」的分支。讀到底,但對方永遠不結束。
advisory 還補了一個對照:同一個子系統的其他讀取,早就有上限了。/api/fetch 是 MAX_PROXIED_RESPONSE_BYTES,4 MiB;401 和 403 的 body 是 CHALLENGE_BODY_MAX_BYTES,64 KiB。這一條漏掉的,剛好是那個「被判定不會久留」的分支,沒有人替它放溢流孔。
PoC 的數字是 advisory 作者自述,我沒跑過:512 MiB 的容器,記憶體 t=0s 是 82.4MiB,t=23s 是 464.4MiB,t=29s 容器結束,ExitCode=139,log 是 FATAL v8::internal::V8::FatalProcessOutOfMemory。對照組是 server 回覆後就關閉串流,記憶體約 50MiB 持平。release notes 的形容更嚴重一點:這樣會 “take down every session”,因為是 web backend 本身死掉,不是單一連線。
這個洞有個前科,advisory 自己寫了:GHSA-m5pj-3536-3xcp 在 2.12.0 修過同一類問題,對象是 POST /api/fetch;而 /api/mcp/* 這條傳輸有另一處同樣沒設上限的讀取,在 2.13.0 還在。2.15.0 的 release note 寫的修法是 “The network tracker caps the body it records for a POST event-stream response”,說的是網路追蹤器。至於 /api/mcp/* 那一處在 2.15.0 之後是不是被同一個修正涵蓋,我沒有查證,不下結論。
預覽畫面說的,和實際存下來的
第三個洞比較安靜。GHSA-pp85-586h-2m95,severity low,CVSS 欄是 null,只影響 2.14.0 這一版。
Inspector 可以匯入一份 server.json 來新增 server,匯入時會跳一個啟動預覽。advisory 的描述是:預覽只列環境變數的名稱,但 environmentVariables[].default 與 .value 會原樣複製進存下來的設定的 env。範例是預覽顯示 npx -y @modelcontextprotocol/[email protected],看起來是個普通指令,但 NODE_OPTIONS=--import=data:text/javascript,… 會在 npx 取包之前就執行程式碼。
advisory 附了一段本機對照:NODE_OPTIONS="--import=data:text/javascript,console.error('PWNED_BY_ENV')" npx --version 先印出 PWNED_BY_ENV,才印出 10.9.9。也是作者的描述,我沒跑。
作者列出的、會改變執行內容的變數包括 NODE_OPTIONS、npm_config_registry(及 NPM_CONFIG_REGISTRY)、UV_INDEX_URL、UV_DEFAULT_INDEX、PIP_INDEX_URL、DOCKER_HOST、PATH、LD_PRELOAD、DYLD_INSERT_LIBRARIES。
作者對自己這份報告也很誠實,減輕因素寫在同一份裡:”It needs the user to import an untrusted server.json and click Add, and the values are visible in the Environment Variables inputs.” 它被私下回報的理由是 “a gap in the GHSA-rwv7 fix rather than a new class of bug”。意思是 2.14.0 才剛為 server.json 匯入補了一輪(GHSA-rwv7-mh2c-ff2p,published 2026-11-05),同一條匯入路徑上,預覽又留了一個縫。
2.15.0 的修法有三件事:預覽顯示每個環境變數的值,把 NODE_OPTIONS、npm_config_registry 這類會控制 runtime 的變數用警告標起來,而且要使用者確認才能加入。順帶一提,advisory 還說 isSecret: true 被解析了,但匯入面板沒用到,secret 的預設值會以明文 TextInput 顯示。這一條我只在 advisory 裡讀到,release notes 沒有提。
只有 Windows 才成立的那個前提
最後一個,GHSA-5572-x678-xmv8,CWE-863,metadata 是 medium、CVSS 5,影響 2.10.0 到 2.14.0。
release notes 的寫法是 “an impostor listening on the daemon’s socket or pipe”,讀起來像是兩個平台都有事。但 advisory 的攻擊情境只寫了 Windows:IPC endpoint 是一個由 SHA-256 推出來的 16 位 hex 具名管道,名稱只取決於可預測的 daemon 目錄路徑。Windows 的具名管道活在整台機器共用的 namespace,誰先建誰贏,所以本機的攻擊者可以先把 \\.\pipe\mcp-conn-<hash> 建起來,冒充 daemon,從 client 手上收走 bearer token。Unix 那邊是 daemon 目錄裡的 mcpdod.sock,目錄要求 0700 且屬於當前使用者,advisory 沒有把它當攻擊面。
2.15.0 的修法是 client 先驗 daemon:daemon 得證明自己持有這位使用者的 token,冒牌的那個什麼憑證都拿不到。證明的細節我沒讀。
把四件事擺回時間軸
四則 advisory 都在 2026-11-12 的 01:33 到 01:34 UTC 發出。把前後的版本排一下:
MCP Inspector 的安全修正節奏
2026-10-21 2.12.0
同日發出 GHSA-m5pj-3536-3xcp,POST /api/fetch 的回應大小上限,severity medium。
2026-11-05 GHSA-rwv7-mh2c-ff2p
server.json 匯入的 runtimeArguments、identifier 與 URL scheme 未驗證,severity medium,修在 2.14.0(2.14.0 本身發布於 11-04,UTC)。
2026-11-11 1.0.5
v1 線的安全版本,補上 100.100.100.200 與 192.0.0.192。
2026-11-12 2.15.0
一次修四個:fetch guard、事件串流、server.json 預覽、mcpdo。
版本日期取自 GitHub API 的 release 時間(UTC),2.8.0 到 2.15.0 大約每週一版。
把這條線看過一遍,我的歸納是:這個專案在同一個地方反覆被補。無上限讀取在 /api/fetch(2.12.0)補過,一個月內又在事件串流出現;server.json 匯入在 2.14.0 補過,一週後預覽又補一次。作者很認真,每一則 advisory 都寫得很細、修得很快。我看到的模式是:每次補的範圍都停在出事的那個點,沒有往「所有會讀上游資料的點」延伸。
這是我的歸納,證據只有 advisory 自述的因果。如果 v2 的程式碼其實把所有讀取收進同一個有上限的 helper,上面這段我就收回;我沒讀到那一層,所以只能說這樣看起來。
帶得走的檢查
四個洞拆完,我會拿同一組問題去看任何一個「連到不可信上游」的工具。
守衛是列舉還是區間?區間可能誤傷,列舉靠人維護,兩邊都有代價,重點是知道自己選了哪一邊、漏的會是什麼。每一個讀取點有沒有上限?「大部分都有」不算,要找那個沒設的分支,它的名字通常叫「這個不會久留」。預覽畫面顯示的,是不是就是實際存下來、之後會執行的那一份?還有,兩端互相驗證的時候,誰先出示憑證,那個位置有沒有人能搶先站進去。
版本現況(npm view @modelcontextprotocol/inspector dist-tags,2026-11-12 取得):latest 是 2.15.0,next 是 2.0.0-rc.3,v1-latest 是 1.0.5。升級是 npx @modelcontextprotocol/inspector@latest,v1 線用 @v1-latest。四則 advisory 的 cve_id 在我查的當下都是 null,有沒有人被實際利用,我手上沒有任何來源,所以不寫。










