← 返回所有文章
發佈於 · 作者 Renaud Deraison

登入連結其實是一道指令

10 月 19 日,Pillar Security 揭露 opencode 在 Windows 與 WSL 上的一個遠端程式碼執行漏洞。遠端 MCP 伺服器可以把一段 PowerShell 子運算式塞進一個普通的 https:// 登入網址,而 opencode 的瀏覽器開啟程式會在 OAuth 交握期間執行它。修正以重構的形式發布,沒有 CVE,也沒有安全公告。在 Bromure Agentic Coding 裡,MCP 登入由 Mac 自己完成,而來自 VM 的網址是以文字形式抵達 Mac,沒有任何 shell 會去讀它。

登入頁面是伺服器挑的,而那個頁面的網址裡藏著一道指令。代理開啟了它,瀏覽器還沒出現,PowerShell 就已經執行了那道指令。

你在程式設計代理裡加入一個遠端 MCP 伺服器。MCP,也就是 Model Context Protocol,是代理連到外部工具的方式:議題追蹤系統、資料庫、文件索引。伺服器要求你登入,於是代理執行 OAuth 交握。它問伺服器登入頁面在哪裡,拿到一個網址,然後在你的瀏覽器裡開啟。你從頭到尾都沒碰過那個網址。替你省下這次複製貼上,正是交握的用意。

10 月 19 日,Pillar Security 發表了 Eilon Cohen 撰寫的 Open and Shut: Remote Code Execution in opencode via MCP OAuth。opencode 是一個開源的程式設計代理,每週在 npm 上約有 300 萬次下載。在 Windows 與 WSL 上,1.0.137 到 1.18.32 版會在這次交握期間執行 MCP 伺服器選定的指令,不需要任何確認,也不需要點擊。Pillar 建議的 CVSS 分數是 9.8。Infosecurity Magazine 在 10 月 20 日做了報導。

從探索文件到 PowerShell

網址由伺服器決定。在 OAuth 交握中,伺服器的探索文件會指定一個 authorization_endpoint,那是一個格式不受限的字串,而用戶端會把你送過去。Pillar 追蹤了它在 opencode 1.18.32 裡經過的路徑:

server's discovery doc → MCP SDK → oauth-provider.ts → mcp/index.ts → browser.ts → open() → PowerShell

這條路徑上沒有任何地方檢查網址的 scheme,也沒有拿網址去和你設定的伺服器比對。open 是一個很熱門、用來啟動瀏覽器的 npm 套件,每週約有 1.84 億次下載。在 macOS 和 Linux 上,它把網址當成單一參數交給 open 或 xdg-open,所以沒有 shell 會讀到它。在 Windows 上,以及會把工作轉交給 Windows 主機的 WSL 裡,10.1.2 版會組出一行 PowerShell 指令。它會跳脫應用程式名稱,卻把網址原封不動地放在雙引號裡。PowerShell 會展開雙引號字串裡的 $(...)。Pillar 的酬載如下:

https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))

PowerShell 在嘗試啟動任何東西之前就先求值這段子運算式。等到瀏覽器啟動失敗時,檔案已經寫進磁碟,而 opencode 把錯誤丟掉了。

這個網址是以 https:// 開頭的。在 mcp-remote 出現一模一樣的漏洞(CVE-2025-6514,2025 年 7 月)之後,標準建議是只接受 http: 和 https: 連結。MCP 的 TypeScript SDK 會封鎖 javascript:、data: 和 vbscript:。opencode 自己的修補加上了 http/https 檢查。Pillar 的酬載全部都能通過。真正在 opencode 1.18.33 補上漏洞的,是升級到 open 11.0.4,它用單引號包住網址,而 PowerShell 在單引號裡什麼都不展開。

還有兩個細節讓這件事很難管理:

  • 伺服器可能在你加入之後才變成敵人。 酬載是在連線當下透過 HTTP 回應送來的。一個在你設定那天毫無問題的伺服器,之後可能遭到入侵,它的網域、它在登錄庫裡的條目,或者一個仿冒名稱的複製品也一樣。
  • 沒有任何掃描工具會標出它。 Pillar 在 8 月 29 日透過 GitHub 的私人安全公告回報了這個漏洞。opencode 的開發者 Anomaly 表示惡意 MCP 伺服器不在其威脅模型之內,於 9 月 25 日以瀏覽器啟動程式碼重構的名義合併修正,並在 9 月 28 日發布 1.18.33。它沒有申請 CVE,也沒有發布公告,所以相依性機器人沒有任何東西可以比對。Pillar 的建議是手動盤點 opencode 的安裝。
一個登入網址,兩種交給主機的方式authorization_endpoint: https://evil.test/$([IO.File]::WriteAllText(…))WSL 裡的 opencode 1.18.32Linux 客體opencode MCP OAuth[email protected]Windows 主機powershell -EncodedCommandStart "…" "https://…$(…)"$(…) 先被展開以你的 Windows 帳號執行指令瀏覽器啟動失敗,錯誤被丟棄網址成了主機上一行指令的一部分。Bromure 機器裡的代理Linux VMxdg-open, $BROWSER→ bromure-open送出檔案網址是一行文字沒有 shell 會讀它你的 Mac僅限 http/https 與公開主機NSWorkspace.open(url)從 VM 到瀏覽器,網址始終是網址。$(…) 始終是純文字。
同一個登入網址,兩台機器。在 WSL 裡,Linux 客體內的開啟程式把網址交給 Windows 主機上的 powershell.exe,後者展開 $(...) 子運算式,並以你的 Windows 帳號權限執行它。在 Bromure Agentic Coding 裡,客體的開啟程式把網址寫進一個給 Mac 的檔案。Mac 只把它當成指向公開主機的 http 或 https 位址來接受,並交給 macOS 自己的網址啟動機制,那個機制不會啟動任何 shell。

WSL 與 Bromure,只差一個設計選擇

WSL 讓這個漏洞變成主機遭入侵。它在 Windows 之上執行 Linux,當裡面的程式想要一個瀏覽器時,請求會送到主機。負責開啟的是 Windows 的 shell,所以藏在網址裡的指令會在 Linux 之外、在你的 Windows 電腦上、以你的 Windows 帳號執行。

Bromure Agentic Coding 的架構相同。它在你 Mac 上的 Linux VM 裡執行 Claude Code、Codex、Grok、Kimi 和 omp,從 VM 裡開啟的登入頁面會出現在 Mac 的瀏覽器中。兩者的分歧點在於交接。

在每一台 Bromure 機器裡,xdg-open 和 $BROWSER 都指向一個名為 bromure-open 的小腳本。Linux 上的 open 套件會呼叫 xdg-open,大多數會「開啟瀏覽器」的 CLI 也是。bromure-open 只做一件事:把網址當成單獨一行文字,寫進與 Mac 共用的資料夾裡的一個檔案。它不會執行或展開其中的任何東西。

Mac 取走這個檔案,刪除它,然後把那一行當成網址來讀。只有指向公開主機的 http 與 https 位址能通過。mailto:、file:、自訂 scheme、localhost 和私有位址都會被丟棄。剩下的會交給 NSWorkspace.open,也就是把網址交給你預設瀏覽器的 macOS 呼叫。在 VM 邊界的兩側,都沒有 shell 參與。

Pillar 的網址以網址的身分抵達你的瀏覽器,$([IO.File]::WriteAllText(…)) 只是路徑裡的一段純文字。瀏覽器會開啟 evil.test 上的一個頁面,或者開不起來,而沒有任何指令被執行。

MCP 登入由 Mac 完成

使用 Bromure 內建的代理時,登入網址根本不會進到 VM。遠端 MCP 伺服器只要在機器的 MCP 面板裡加入一次。Bromure 會把每一個轉換成 Claude Code、Codex、Grok、Kimi 和 omp 的設定格式,在開機時交給 VM。

對於 HTTP 伺服器,Authorize with OAuth… 會在 Mac 上執行登入。Bromure 的仲介程式會抓取伺服器的探索文件,以 Bromure AC 的名義註冊自己,用 Apple 的 URL API 組出授權網址並以 NSWorkspace.open 開啟,接著在 Mac 的 loopback 連接埠上接收回呼。權杖留在 Mac 上。代理的設定裡不含任何憑證,而送往該伺服器的請求,由代理伺服器加上授權標頭。

代理伺服器也會親自回應對該伺服器的 OAuth 探索請求。代理看到的是一個它已經登入的伺服器,所以它從不啟動自己的交握,從不向伺服器要 authorization_endpoint,也從不把它交給開啟程式。Pillar 利用的那條程式碼路徑根本不會執行。

連線到哪裡,不由 VM 決定

Pillar 警告,伺服器可能在你加入之後才變成敵人。Bromure 從 Mac 端控制 VM 能連到哪些伺服器。機器發出的每一條連線都會經過一台虛擬網路交換器和一個代理伺服器,兩者都在 VM 之外。在機器的 Guardrails 面板裡,把 Unmatched traffic 設為 Deny,再列出你使用的 MCP 伺服器:

allow web mcp.linear.app
allow web mcp.notion.com
allow web registry.npmjs.org
allow web github.com
allow web api.anthropic.com
default deny

不在清單上的伺服器,例如仿冒名稱的網域,或某人從論壇貼來的條目,根本無法提供探索文件,所以也就沒有 authorization_endpoint 可開。交換器會依 IP 以及它在 DNS 中看到的主機名稱檢查每一條連線,代理伺服器再依 TLS 伺服器名稱檢查一次。每一次拒絕都會成為 Security Timeline(Window → Security Timeline…)裡一列紅色的 deny,記下主機、連接埠與機器。規則在你按下 Save 的那一刻就生效,VM 裡的任何東西都無法修改它們。

在 VM 裡,每一把金鑰都是替身。ANTHROPIC_API_KEY、gh 權杖、AWS 密鑰和 MCP 靜態權杖裝的都是假值,代理伺服器只有在請求送往該金鑰所屬的主機時才換上真的。如果某道指令收集這些值,並把其中一個送到不屬於它的主機,入侵偵測器會回應 HTTP 451,什麼都不轉送,並以 This environment may have been compromised 暫停這台機器。

Bromure Agentic Coding 中的遠端 MCPLinux VM代理的 MCP 設定伺服器網址,開機時寫入沒有權杖,無須登入金鑰與權杖只有替身xdg-open → bromure-open網址以文字送出你的 MacOAuth 仲介程式探索、PKCE 登入、權杖留在 Mac 上Proxy回應代理的探索請求在線路上加入真正的權杖allow web mcp.linear.appallow web mcp.notion.comallow web api.anthropic.comdefault denymcp.linear.app在清單上,以真正的權杖連上mcp-linear[.]app不在清單上:在交換器被拒Timeline 上的一列 deny
在 Bromure 裡加入的 MCP 伺服器。Mac 的 OAuth 仲介程式負責登入並保管權杖。代理伺服器回應代理的探索請求,所以代理從不執行自己的交握,也從不開啟伺服器提供的網址。Mac 上的允許清單限制 VM 究竟能連到哪些 MCP 主機,每一次拒絕都有紀錄。

找出沒有掃描工具會標出的版本

既然沒有 CVE,Pillar 的建議是自己盤點 opencode 的安裝。在 Bromure 裡,這些紀錄早就有了。

找出版本。 無論供應鏈設定為何,Security Timeline 都會記錄每一次套件下載,並在 Condition 欄位寫下套件與版本。在 Filter events… 輸入 opencode-ai,任何寫著 npm · [email protected] 或更舊版本的列,都會指出是哪台機器。Export… 可把這些列存成 CSV。在已註冊到 bromure.io 的 Mac 上,每一次下載也都是一筆 supply_chain.fetch 事件,所以管理員可以從單一主控台,在所有開發者的機器上搜尋 opencode-ai,不必逐一詢問每個團隊。

找出伺服器。 bromure-cli trace hostnames 會列出機器連過的每一台主機,以及請求次數。你的代理在對話的 MCP 主機都在這份清單上,包括那些從未加進 MCP 面板的。該審查的,或者該排除在允許清單之外的,正是這些。

在 VM 裡升級。 在機器的終端機執行 npm install -g opencode-ai@latest,就會裝上 1.18.33 或更新版本,而新的下載會出現在 Timeline 上作為證明。

網址以文字形式離開 VM

xdg-open 和 $BROWSER 會把網址寫進給 Mac 的檔案。Mac 只接受指向公開主機的 http 或 https,並在不經過 shell 的情況下開啟。

MCP 登入在 Mac 上

仲介程式執行 OAuth 並保管權杖。代理伺服器回應代理的探索,所以代理從不開啟伺服器提供的登入網址。

MCP 主機的允許清單

把 Unmatched traffic 設為 Deny 後,只有你列出的伺服器連得到。每一次拒絕都是 Security Timeline 上的一列。

沒有 CVE 也能盤點

每一次套件下載都連同版本記錄下來,機器連過的每一台主機,一道指令就能列出。

每個欄位都不可信

Pillar 最後的教訓是寫給打造 MCP 用戶端的人:伺服器送來的每個欄位都是不可信的輸入,而且大部分早在模型讀到任何東西之前就已經被解析了。在這個漏洞裡,模型從頭到尾沒看過酬載,執行它的是一個瀏覽器啟動程式。

你不可能稽核你安裝的每個代理裡的每個啟動程式,而這一個在沒有通知任何人的情況下就修好了。Bromure 把決定移出 VM。登入、開啟網址、決定 VM 能連到哪些主機的,都是 Mac。真正的憑證也在 Mac 手上。關於 MCP 伺服器本身就是攻擊者的更早案例,請讀〈模型就是他們的伺服器〉。關於信任了錯誤來源的代理設定,請讀〈你信任的是工作區,不是那些伺服器〉。