返回所有文章
发布于 · 作者 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 表把 它扩展成笔记本的配置。

这张表的两个性质造成了伤害,而两者单独看都不是 bug。

第一个是: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 版把运送工具 从一个仓库缩成一段注释。

信任单位一直在变小

这类 bug 的历史很短,而在这段历史里,攻击者每一次需要你机器的部分都更少。

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 那个 bug 之所以痛,是因为被启动的 服务器是开发者真实 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。