那个笔记本自带了一台 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_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 版把运送工具
从一个仓库缩成一段注释。
信任单位一直在变小
这类 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 服务器的来源,所以没有一场合并会把优先级搞错,也没有一个清理器要跟着一份不断 长出新段落的配置模式保持同步。一份文档加不了条目到一份它够不着的清单里。
那就照样把笔记本打开
在你正跑着有漏洞版本的那一天,上面说的都帮不上忙。所以拿 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 风格的规则表:一个动作、一个协议
(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。