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

那個筆記本自己帶了 MCP 伺服器來

CVE-2026-75149 是 marimo 的程式碼注入漏洞:一個筆記本就是一個 Python 檔案,這個檔案把自己的設定裝在註解區塊裡,而在 0.23.15 之前,那份設定可以宣告一個 MCP 伺服器。你用編輯模式打開筆記本,marimo 就會把攻擊者的指令當成子行程啟動,而且是在任何一個 cell 執行之前。剩下的話由修補說完了:它一口氣拿掉五整類環境設定——那是一份下載來的文件原本有權設定、而且優先權還高過操作者自己設定的東西。在 Bromure Agentic Coding 裡,你的代理可以啟動哪些程式,是一份主機端的面板,開機時寫進 VM,工作區裡的任何東西都加不進去。

一個儲存庫可以把你的編碼代理重新設定過。這件事我們早就知道。變的是能做到 這件事的東西有多大。現在它是一個檔案,從 Slack 轉發給你,你打開只是想看 一眼。

有人傳了一個筆記本給你。那是 marimo 筆記本,所以它是一個普通的 .py 檔案, 而不是一包塞著 base64 輸出的 JSON——人們喜歡這個格式,大半就是為了這點。你 讀得懂它,你 diff 得了它,而且不用裝一個剝掉輸出的 pre-commit hook 就能把它 放進 git。

於是你讀了,看起來沒問題,你用編輯模式打開它,想動一動某個 cell。

一個子行程啟動了。你還沒有執行任何 cell;這是檔案自己要求的事,而且發生在 筆記本載入完成之前。

那就是 CVE-2026-75149, 8 月 19 日發布,本週被 The Hacker News 報導。CVSS 8.7、CWE-94,記在 Gregory Tan 名下,在 marimo 0.23.15 修好。 VulnCheck 的公告 用一句話說完了機制:筆記本裡有一筆動過手腳的 MCP 伺服器項目,用編輯模式 打開它,就會把那筆項目的指令當成本機子行程啟動。

設定寫在註解裡

marimo 筆記本用 PEP 723 帶著自己的設定 走,那是 Python 用來寫行內腳本中繼資料的慣例。它是一張住在檔案開頭註解區塊裡 的小 TOML 表,存在的目的是讓一支腳本不必在旁邊放一個專案檔,就能宣告自己的 相依套件和 Python 版本。uv 會讀它。marimo 也讀,而且用一張 tool.marimo 表把它延伸成筆記本的設定。

這張表的兩個性質造成了傷害,而兩者單獨看都不是臭蟲。

第一個是:marimo 把筆記本的中繼資料以最高優先權合併進來,蓋過執行 marimo 的人自己設定的東西。對排版偏好來說,那是正確的預設。一個想要兩格縮排的筆記本 本來就該贏過你的全域設定,因為知道自己檔案長什麼樣的是作者,不是你。

第二個是:這場合併幾乎沒有過濾。修好之前,marimo 的清理器只從筆記本提供的 設定裡拿掉一個鍵:tool.marimo.runtime.auto_instantiatetool.marimo 裡 其他東西全部通過,包括 tool.marimo.mcp。那裡就是你列出 Model Context Protocol 伺服器的地方,而一份 MCP 伺服器清單,就是一份要啟動的指令清單。

把兩件事放在一起,一個下載來的檔案,開頭的註解就能點名一支程式,讓 marimo 去啟動它。這裡沒有任何一處算是漏洞利用。檔案就是照著功能被設計的樣子在用, 而那個設計假設了:檔案是你自己寫的。

analysis.py · 一個檔案,轉發到你手上# /// script# dependencies = ["polars", "altair"]# [tool.marimo.mcp.servers.helper]# command = "…"# ///PEP 723 中繼資料 · 註解裡的一張 TOML 表import marimo as mo@app.celldef _(): …那些 cell · 別人叫你要小心的那一半設定合併,0.23.15 之前你的設定 · 優先權最低檔案的設定 · 優先權最高只剝掉一個鍵: runtime.auto_instantiatemarimo edit analysis.pyMCP 伺服器項目 → 本機子行程在任何 cell 執行之前繼承打開它的那個 shell這個格式教會你的習慣打開筆記本,讀那些 cell,決定要跑哪些。先檢查,後執行——這正是這個格式存在的全部理由。那筆 MCP 項目,是在第一步的時候跑掉的。
那道指令是從哪裡來的。marimo 筆記本是一個 .py 檔案,設定寫在開頭的 PEP 723 註解區塊裡。0.23.15 之前的 marimo 只拿掉一個鍵,就把那個區塊合併到操作者自己的設定之上,於是註解裡的 mcp 區段就變成一支程式,在編輯模式打開的當下、任何 cell 執行之前,由 marimo 啟動。

修補本身就是揭露

修補 PR #10281 的標題是 「additional pep 723 sanitization」,它把封鎖清單換成了允許清單。筆記本提供 的設定,現在只限於外觀與編輯器相關的區段:格式化、儲存、顯示、快捷鍵、 診斷、lint、片段、資料來源、語言伺服器、分享、venv、執行環境、套件管理。

被拿掉的那些區段才是有趣的一半:aimcpcompletionsecretsserver

那是五個完整的區段,不是五個鍵,而且一份你下載來的文件全部都設定得了。這個 PR 的說明還點出了第二種酬載,那種連子行程都不需要。ai 區段帶著 API 的 base URL,所以一個筆記本可以把模型端點改指到自己挑的主機上,用維護者的話 說,這「可能讓操作者的 API 金鑰被外洩到攻擊者控制的端點」。

這種攻擊的 base URL 版本我們在 7 月寫過,那次是 一個儲存庫可以設定 ANTHROPIC_BASE_URL, 而 Claude Code 就把自己的金鑰寄到儲存庫挑的位址去。它是這個類別裡最便宜的 攻擊,因為沒有任何程式碼被執行,動手送出的是受害者自己的用戶端。marimo 版 把運送工具從一個儲存庫縮成一段註解。

信任單位一直在變小

這類臭蟲的歷史很短,而在這段歷史裡,攻擊者每一次需要你機器的部分都更少。

4 月,Wiz Research 通報了 Amazon Q Developer 的 CVE-2026-12957, CVSS 8.5,5 月修補,6 月揭露。Amazon Q 會從打開的工作區讀 .amazonq/mcp.json 並啟動裡面定義的伺服器。那些行程繼承了開發者的完整環境:AWS 金鑰、雲端 CLI 權杖、API 機密、SSH agent socket。Amazon 的修法是:從不受信任的工作區啟動 MCP 伺服器之前,先問一句。

8 月,ChainDrop 把代理設定 commit 進它爬過的儲存庫.claude/settings.json 裡的 SessionStart hook,和 .vscode/tasks.json 裡 的 folderOpen 任務,於是把專案 clone 下來再打開就夠了。

那兩件事都需要一個儲存庫。得有人發布或攻陷一個專案,你得把它 clone 下來,而 且一整個 dotfile 目錄就擺在樹裡,一個多疑的人可以打開來看。

marimo 只需要一個檔案。一個檔案,它以文件的樣子呈現,也以文件的方式抵達:一 條 Slack 訊息、一封信的附件、有人在站立會議上貼的 gist、從 Kaggle 下載的 東西。筆記本沒有人在 clone。你打開它,因為打開就是你弄清楚它是什麼的方法。

mcp 區塊在過去十八個月裡擴散到了大多數開發工具,而這些區塊住在人與人之間 流動的檔案裡:儲存庫根目錄、編輯器目錄,現在還有文件的中繼資料。每一個都是 一份你機器上某個東西願意去啟動的程式清單,而寫的人是最後動過那個檔案的人。 登錄檔掃描器不會讀它們,鎖定檔涵蓋不到它們,也還沒有人提出過一種能套用在 註解上的來源證明。

在一個 profile 裡,MCP 伺服器從哪裡來

Bromure Agentic Coding 在能回答來源問題的地方回答它:在 主機上,在 VM 開機之前。

每個 profile 的設定裡都有一個 MCP 面板,跟 Agents、Credentials、 Guardrails、Supply Chain 並排。它裝著一份伺服器清單。每一筆項目都能各自開關, 並使用兩種傳輸之一:HTTP,一個遠端 URL 加上選填的 bearer token;或 stdio,一道在 VM 裡啟動的本機指令。Bromure 會把那份清單翻成當前代理期待 的格式——Claude Code 用 JSON,Codex 用 TOML——然後在開機時注入 VM。

讀一下最後那句話的方向。設定是主機進到客體裡,在開機時,來源是 一份你在設定面板裡維護的清單。之後才出現在工作區裡的檔案,並不是 MCP 伺服器 的來源,所以沒有一場合併會把優先權搞錯,也沒有一個清理器要跟著一份不斷長出 新區段的設定綱要保持同步。一份文件加不了項目到一份它構不著的清單裡。

設定跟著檔案一起旅行誰在寫這份伺服器清單?最後編輯過你打開那個檔案的人它什麼時候被讀?打開的當下,在你決定任何事之前誰蓋得過誰?下載來的東西蓋過你自己的設定子行程繼承到什麼?你打開它的那個 shell,以及隨之而來的~/.aws · ~/.ssh · SSH_AUTH_SOCK · API 金鑰設定住在 profile 裡誰在寫這份伺服器清單?你,在 profile 的 MCP 面板裡,在主機上它什麼時候被讀?開機時,翻成當前代理的格式誰蓋得過誰?工作區裡的東西根本不在合併之列子行程繼承到什麼?一台用完即丟 VM 的環境:誘餌金鑰、沒有 SSH 私鑰,出站由主機端過濾
同一個問題的兩種答案:誰可以往你的工具會去啟動的東西清單裡加一支程式?左邊是最後編輯過你打開那個檔案的人,以更高的優先權合併到你自己的設定之上,而且是在一台放著你雲端憑證的機器上。右邊是你,在主機的設定面板裡,開機時寫進一台用完即丟的 VM,那裡端出來的憑證都是誘餌,而且有一張客體改不了的表在過濾對外連線。

那就照樣把筆記本打開

在你正跑著有漏洞版本的那一天,上面說的都幫不上忙。所以拿 0.23.15 之前的 marimo,把勝利送給攻擊者,讓一個敵意筆記本走一趟 profile。

子行程是在別人的電腦裡開始的。 Bromure Agentic Coding 的工作階段發生在 profile 的 Ubuntu VM 裡的 kitty 分頁中,離 macOS 隔著一層 hypervisor。 marimo edit 在那裡跑,註解裡那道指令也在那裡跑。在 Resources → Storage 底下,profile 是三層:每個 profile 各自的 /home/ubuntu,帶著 Erase home…;工作區系統磁碟,帶著會從共用映像重新 clone 的 Reset to base…;再下面是一層唯讀的基底 OS,客體裡的任何東西都碰不到。一個 選單項目就把那筆項目裝的東西全部帶走。

它繼承到的環境是一整組誘餌。 Amazon Q 那個臭蟲之所以痛,是因為被啟動的 伺服器是開發者真實 shell 的子行程,繼承了開發者真實的憑證。在 Bromure 的 profile 裡,那些變數名稱後面什麼都沒有。通用 API 金鑰是匯出到 VM 裡的 brm_… 佔位符,在往外送的路上由主機的 proxy 換成真值。kubeconfig 是合成的, 用的是用完即丟的用戶端憑證。AWS 請求在主機端重新簽章,所以繞過 proxy 的行程 拿到的是 InvalidSignatureException,不是存取權。~/.docker/config.json 裡 放的是一坨假的 base64。SSH 私鑰 根本從來不在 VM 裡,因為主機透過 一個 per-profile 的 agent 代簽。對某一項憑證打開 Require approval to use, 每一次替換就變成主機上的一個對話框,而且授權有時限:五分鐘、一小時、這次 工作階段剩下的時間。

base URL 那一招收到的是佔位符。 proxy 只有在往那項憑證自己的主機送出的 請求上,才會換上真的憑證。一個把 AI 端點改指到 attacker.example 的筆記本, 收到的是 VM 環境裡擺著的東西,也就是一串什麼都打不開的 brm_…

伺服器得摸得到網路。 一筆會生出對攻擊者有用東西的 MCP 項目,需要出站 連線,不管是去拉後面的階段,還是把找到的東西送回去。Guardrails → Outbound connections 是一張 pf 風格的規則表:一個動作、一個協定 (tcpudpwebany)、一個主機或 CIDR、一串連接埠,web 的話還有 一串 HTTP 方法,由上往下評估,先命中者勝,外加一個 Unmatched traffic 設定 可選 Allow 或 Deny。把它設成 Deny,列出你工作需要的主機,那筆項目的目的地就 不在清單上。這張表是在主機端、在虛擬交換器和 proxy 裡執行的,所以客體對自己 的路由做什麼都改不了答案。

首先,檔案得在 VM 裡才行。 一個 profile 最多分享八個 Mac 資料夾,每一個 都要在 Folders 面板裡親手挑,掛在 /home/ubuntu/<basename>。掉進 ~/Downloads 的筆記本,在你把它放進去之前並不在 profile 裡,而你 Mac 上其他 的東西,也不是從那個筆記本啟動的玩意兒走一趟目錄就到得了的。

而且你看得見它發生。 Security Log 視窗(Window → Security Log…)是 主機端單一的一張時序表:套件裁決、防火牆決定、憑證替換、提示注入偵測。客體 程式碼改不了它,因為客體程式碼構不著它。一條來自你沒啟動過的行程、被擋下來 的出站連線,會在那裡變成一行,這比六週後從雲端帳單上才知道好。

把它打開

把 marimo 更新到 0.23.15 或更新的版本;那裡的允許清單是個好修補。然後去看看 你的工具還會從哪些地方讀 mcp 區塊,並且問一句:誰有權寫那個檔案。

在一個 profile 裡,值得花兩分鐘的設定還是同一份短清單。Guardrails → Outbound connections,把 Unmatched traffic 設成 Deny,再給你工作需要的 主機一份允許清單。Credentials → Require approval to use,套在任何會花錢 或會刪資料的東西上。Supply Chain → OSV vulnerability check,有金鑰的話再 加上 socket.dev 或 Delpi 的過濾,疊在預設就開著的兩天年齡門檻之上。Prompt Injection → CLAUDE.md 與 AGENTS.md 掃描器,它把那些自稱對你的代理有權威的 檔案,就當成那麼一回事來處理。

筆記本還是會一直被轉發,設定格式也還是會一直長出新的區段,因為這兩件事都 有用。你能改的,是當一份文件開口要求啟動一支程式時,聽著的是哪一台機器。 裝上 Bromure Agentic Coding,把你的伺服器清單留在 profile 編輯器裡,讓下一段熱心的註解區塊去設定一台你從選單就能抹掉的 VM。