那個筆記本自己帶了 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_instantiate。tool.marimo 裡
其他東西全部通過,包括 tool.marimo.mcp。那裡就是你列出 Model Context
Protocol 伺服器的地方,而一份 MCP 伺服器清單,就是一份要啟動的指令清單。
把兩件事放在一起,一個下載來的檔案,開頭的註解就能點名一支程式,讓 marimo 去啟動它。這裡沒有任何一處算是漏洞利用。檔案就是照著功能被設計的樣子在用, 而那個設計假設了:檔案是你自己寫的。
修補本身就是揭露
修補 PR #10281 的標題是 「additional pep 723 sanitization」,它把封鎖清單換成了允許清單。筆記本提供 的設定,現在只限於外觀與編輯器相關的區段:格式化、儲存、顯示、快捷鍵、 診斷、lint、片段、資料來源、語言伺服器、分享、venv、執行環境、套件管理。
被拿掉的那些區段才是有趣的一半:ai、mcp、completion、secrets、
server。
那是五個完整的區段,不是五個鍵,而且一份你下載來的文件全部都設定得了。這個
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 伺服器 的來源,所以沒有一場合併會把優先權搞錯,也沒有一個清理器要跟著一份不斷長出 新區段的設定綱要保持同步。一份文件加不了項目到一份它構不著的清單裡。
那就照樣把筆記本打開
在你正跑著有漏洞版本的那一天,上面說的都幫不上忙。所以拿 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 風格的規則表:一個動作、一個協定
(tcp、udp、web、any)、一個主機或 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。