返回所有文章
发布于 · 作者 Renaud Deraison

沙箱把端口打开了

CVE-2026-65105 是 NVIDIA NemoClaw 里的一个网络缺陷,而这个工具存在的全部意义,就是把 AI 代理放进沙箱里跑。沙箱是 Docker 容器,容器够不着宿主机的 loopback 接口,于是 NemoClaw 改成把本地模型服务器开在所有接口上——而这恰好关掉了唯一一道能拦住网页跟它说话的检查。来访的网页拿走的,是模型的对话模板,也就是在模型读到之前,把每一段系统提示词拼装起来的那一层。在 Bromure Agentic Coding 里,本地引擎绑在 loopback 上,用的端口客户机永远不会知道,而客户机是通过 vsock 而不是网络抵达它的。

把洞开出来的,正是围堵本身。一个够不着你机器 loopback 接口的沙箱,会把模型服 务器逼到所有接口上,而从那一刻起,浏览器标签页也够得着它。

你正用最谨慎的方式跑你的编码代理。它住在沙箱里。它用来思考的模型跑在你自己的机 器上,所以没有任何提示词离开这栋楼。大家让你做的那两件事,你都做了。

然后你开了一个浏览器标签页,从那一刻起,你的代理开始写出带着缺陷的代码,而且对 那个缺陷绝口不提。

那就是 CVE-2026-65105,8 月 25 日由 Oasis Security 的 Elad Luz 与 Ofek Itach 披 露,他们的 研究报告 称之为顺道式的代理劫持。他们在公开前先送交了 NVIDIA 的 PSIRT,同一天 The Hacker NewsSiliconANGLECSO Online 都报道了这件事。NVIDIA 发了 一则安全公告 和一个补丁。

产品叫 NemoClaw,是 NVIDIA 用来把 OpenClaw 代理部署进 OpenShell 沙箱、并配一个本 地 Ollama 后端做推理的工具。围堵就是它的卖点。

容器够不着 loopback

Ollama 监听在端口 11434,默认绑定 127.0.0.1——loopback 接口,只有机器自己够得 着。

OpenShell 的沙箱是 Docker 容器。容器有自己的网络命名空间,所以容器里的 127.0.0.1 是容器,不是宿主机。因此沙箱里的代理够不着绑在宿主机 loopback 上的模 型服务器。

NemoClaw 用一个环境变量解决了它。它以 OLLAMA_HOST=0.0.0.0:11434 启动 Ollama: 机器上的每一个接口,包括朝着办公室 wifi 的那一个。而在 socket 对全世界应答的同 时,安装程序还一直打印着 Using Ollama on localhost:11434

接着就是把一个配置选择变成漏洞的那一步。Ollama 的 API 没有认证,于是改带了两道守 卫。第一道是对 Origin 头的 CORS 检查。第二道是 Host 头的允许列表,接受 localhost、机器自己的主机名,以及以 .localhost.local.internal 结尾 的名字。

第二道守卫里有一个例外。Ollama 会看自己绑在哪个地址上,而如果那个地址不是 loopback,它就整个跳过 Host 检查。 你看得出为什么有人会这么写:绑公网接口的运 维要的就是远程客户端,而一份为 loopback 设计的 Host 列表会把他们全都挡在门外。 结果就是:把端口暴露出去的那个设置,跟解除保护它那道检查的设置,是同一个。

剩下的只有 CORS 检查,而 DNS rebinding 在构造上就能穿过 CORS。攻击者从自己控制的 域名上提供网页,端口 11434,然后让那个域名重新解析到 127.0.0.1OriginHost 现在是同一个攻击者选定的主机名,Ollama 把它读成同源。 rebinding 的机制我们上周已经详细写过,就当 成前提吧:你访问的一个网页,变成你机器上某个服务的客户端,大约只要一分钟。

rebinding 是有意思的那条入侵路径,也是更难的那条。一台开在 0.0.0.0 的服务器根 本不需要任何浏览器把戏,就会应答同一网段上的其他每一台设备:访客的笔记本、智能电 视、隔两张桌子那台被拿下的机器。

约束条件OpenShell 沙箱 = Docker 容器有自己的网络命名空间,所以127.0.0.1 = 容器,不是宿主机代理够不着模型服务器真实的问题,需要真实的答案NemoClaw 交出的答案OLLAMA_HOST=0.0.0.0:11434每一个接口,没有认证安装程序打印:“Using Ollama on localhost”Ollama 的两道守卫1 · 对 Origin 头的 CORS 检查2 · Host 允许列表:localhost、主机名、.localhost .local .internal绑定不是 loopback 时,第 2 道被跳过两条路进得来网页:把域名重绑到 127.0.0.1,于是Origin 与 Host 相符,CORS 过关局域网:直接拨那个端口,不用花招两者都免认证抵达整套 API把端口暴露出去的那份配置,就是关掉保护它那道检查的同一份配置。
端口为什么挪了地方。沙箱是 Docker 容器,够不着宿主机的 loopback 接口,NemoClaw 便以把模型服务器绑到所有接口来解决。只要绑定地址不是 loopback,Ollama 就跳过它的 Host 头检查,于是只剩下一道 CORS 检查——而 DNS rebinding 会让 Origin 和 Host 变成同一个攻击者选定的名字。

他们要的不是代码执行

够到 API 是无聊的那一半。Oasis 拿到之后做的事,才是新东西。

显而易见的一招,是把恶意的系统提示词写进模型里。这在这里行不通:OpenClaw 每次请 求都在 messages 数组里送出自己的系统提示词,而客户端的提示词会压过存在模型里的 那一份。于是研究者往下走了一层。

Ollama 的 /api/create 端点接受一个 template 字段。对话模板是一份 Go 模 板,把结构化的 messages 数组——角色、内容、工具定义——变成模型真正读到的那一整 块平铺文本。它在推理时执行,在客户端交出消息之后,而且每一条它都会碰到。

攻击只有三个请求。用 /api/show 读出真正的模板。工具的渲染方式和特殊 token 一字 不动地保留,只改系统消息怎么渲染,在后面附上你要的指令。用 /api/create 写回 去。

客户端照样送出它那份用心写过的系统提示词。而模板每一次都把那份提示词包进攻击者的 提示词里,在模型看到它的第一个 token 之前就完成了。用 CSO Online 引述 Elad Luz 的 话说,那道指令坐在“任何护栏或运维所能看到的东西,再下面一层”。

这次修改落在磁盘上的模型定义里,因此它熬过对话、熬过重置、熬过重启。模型的元数 据、大小和列出的能力都一样,在已安装模型的列表里看不出任何不同。那个模型的每一个 使用者都会拿到它:你手动敲的 CLI,还有你去吃午饭时仍在跑的代理。

Oasis 列出了这样一道指令能拿来做什么。没有一项戏剧性到会被注意。

把 bug 写进去

产出带着细微缺陷的代码——那种在评审时读起来很顺的缺陷,因为它本来就是为了在 评审时读起来很顺而写的。

保持安静

永远不指出任何安全隐患。一个原本会提醒你、如今不再提醒的代理,是很难察觉的。

引导选择

推荐某个特定的包、某个特定的网址、某个特定的配置。你要的是建议;你也得到了建 议。

把东西送回家

在代理有网络访问的地方,把对话内容和文件内容送到攻击者选定的端点。

其余那些免认证的 API 只是配角:/api/generate/api/chat 拿别人的 GPU 跑推 理,/api/pull 把硬盘塞满,/api/delete 毁掉那些模型,/api/push 用受害者的 ollama.com 账号发布,/api/me 则让你知道刚落到谁身上——主机名、公钥,以及登录中 的用户名。

沙箱从来就不是波及范围

Oasis 对于沙箱做到了什么、没做到什么讲得很小心。OpenShell 沙箱是有效的:它隔离文 件系统、网络和进程,并且让端点保持干净。一个代理在组织里之所以值得存在,是因为它 握着版本控制、CI/CD、内部 API、云账号、聊天工具,以及一整叠 MCP 服务器。拿下代 理,你就指挥得动那些访问权。报告自己的结论是:真正的波及范围跟着代理被授权去够的 资源走,而不是跟着它沙箱的边界走。

而那正是容器永远不会替你回答的问题, 只是这里多了一个转折。造出暴露面的,正是那层围堵。Docker 的网络命名空间做好了它 的工作。绕过它,才把模型服务器摆到了 wifi 上。

本地模型在 profile 里住在哪

Bromure Agentic Coding 也跑本地推理。一个 profile 的代理可以指 向设备上的模型,而不是云厂商,这正是 NemoClaw 提供的同一项功能。底下的管路却是反 过来铺的,而它的每一步,都是对上面某件事的直接回答。

引擎绑 loopback,绝不绑 0.0.0.0 设备上的模型服务器跑在 Mac 上、跑在 127.0.0.1、跑在内核于启动时分配的端口上,而不是一个固定号码。动态端口的理由很 乏味:让 Bromure 永远不会跟那台 Mac 上已经占着传统 11434 的东西打架,包括 Ollama 和 LM Studio。它同时也意味着,没有一个众所周知的端口可以让网页瞄准。

客户机不是通过网络抵达它的。 在 VM 里,代理拨的是 127.0.0.1:11434,正是它 预期的地址。那条连接根本不会靠近任何路由。VM 里的一座桥把 TCP 流接到 vsock ——virtio 的 socket 传输——的 8446 端口上,而通道的宿主机一端把它交给 loopback 上 的引擎。vsock 通道是 hypervisor 与单个客户机之间的一根管子。它没有 IP 地址、没有 主机名、没有 DNS 记录,所以没有东西可以重绑、没有东西可以扫描,浏览器也没有办法 拨它。客户机永远不会知道引擎在哪个端口上。

你局域网上没有任何东西够得着 VM。 在 Resources → Network 底下,默认模式是 NAT:出得去,而你局域网上的任何东西都进不来。这关掉了 NemoClaw 那项发现中完 全不需要 rebinding 的那一半——同一个 wifi 上的设备直接拨那个端口——而且不需要你动 任何设置就关掉了。

VM 里没有模型定义可以被改写。 客户机里的代理被钉在单一模型标识 bromure-local 上,并且以 bromure.llm 这个背后没有 DNS 记录的合成主机名去寻址它。VM 内的代理服 务器拦下这个名字并转给宿主机。哪个模型来应答,是你握在宿主机上的一份映射,就在 profile 的 Local Models 面板里;改动它是宿主机端的重新映射:VM 里不必重新配 置,代理也不必重启。模板、权重和路由全都住在 Mac 上。客户机里没有任何等同 /api/create 的东西,因为客户机里根本没有那个模型。

本地推理走的是跟云端一样的流水线。 因为那些流量走的是 Bromure 的代理服务器, 而不是直接撞上一个端口,它得到的待遇跟调用 Anthropic 或 OpenAI 一样:同一份追踪、 同一份日志、同一套提示词注入检测。Prompt Injection 面板会用本地的 PromptGuard 模 型,为代理读到的文件内容、网页和工具输出打分;并用微调过的 ModernBERT 分类器,加 上一个针对隐形 Unicode 载荷的确定性扫描器,为它当成权威加载的 CLAUDE.md、 AGENTS.md 与 GROK.md 打分。检测结果会进 Security Log,或让请求暂停,或直接回一个 硬邦邦的 451,取决于你选了三种反应中的哪一种。

开在所有接口上的一个端口引擎在哪里监听?0.0.0.0:11434 · 固定、众所周知沙箱怎么抵达它?走 IP,跟 wifi 上其他人一样还有谁够得着?重绑的网页,以及整个局域网,免认证谁能改写对话模板?任何够得着那个端口的东西,通过POST /api/create一条没有地址的通道引擎在哪里监听?127.0.0.1:<内核分配> · 只有宿主机VM 怎么抵达它?vsock 8446——一根管子,不是网络还有谁够得着?没有人:没 IP、没主机名、没 DNS谁来挑模型?你,在 profile 的 Local Models 面板客户机只知道 bromure-local 这个名字
把本地模型交给代理的两种做法。左边是开在所有接口上的 HTTP 端口,没有认证,后面还摆着一份可写的模型定义——网络上任何设备、任何重绑主机名的网页都够得着。右边是绑在内核分配端口上的 loopback 引擎,客户机通过一条没有地址可拨的 vsock 通道抵达它,而模型的选择握在宿主机上。

现在,假设模型正在对你撒谎

一个架构主张推得够远,就该有人去测它。那就用你喜欢的任何路径,把胜利直接送给攻击 者:一份被下毒的模板、模型厂商的一个坏日子、一次穿过来的提示词注入。你的代理现在 替别人干活,而它的输出读起来依然很好。把那四种载荷一一放进 profile 走一遍。

引导选择,而选择照样被审。 这种载荷最倚仗模型的权威,也撞到最多墙。Bromure 在 代理看到响应之前,就在宿主机的代理服务器上评估每一次包的抓取,横跨 npm、PyPI、 Cargo、RubyGems、Maven、NuGet、Go modules 与 Packagist,而且这个代理服务器不会问 是谁建议了这个包。两天的年龄门槛默认开着,刚发布的仿冒包所需要的东西,大半在这里 就被挡下。打开 OSV 查询、把包过滤指向 socket.dev 或 Delpi、剥掉安装脚本——于是一 项推荐必须熬过四种模型完全没有发言权的意见。VM 里的 .npmrcpip.conf 可以 把这些设置收得更紧。它们没办法放松。

把东西送回家,但线是你的。 外泄需要对外连接。Guardrails → Outbound connections 是一张 pf 风格的规则表——一个动作、一个协议(tcpudpwebany)、一个主机或 CIDR、一串端口,web 的话再加一串 HTTP 方法 ——自上而下匹配,第一条命中的获胜,而 Unmatched traffic 设为 Allow 或 Deny。把 它设成 Deny,列出你工作真正需要的主机,攻击者的端点就不在其中。强制是在宿主机上的 虚拟交换机与 SNI 层执行的,所以客户机对自己路由做的任何事都改不了判定。

把 bug 写进去,然后试着把它发出去。 Guardrails 还为代理够得着的每个服务带了一 个模式:对 GitHub、GitLab、Bitbucket、AWS、Kubernetes、DigitalOcean、容器镜像仓库 以及每个配置过的数据库端点,设 Block destructiveRead-only。在只读模式 下,git push 会被归类为写入并被挡下;fetch 永远放行。那个带后门的 commit 就留在 VM 里。

而它继承到的访问权是假的。 Oasis 关于波及范围的那一点,正是 profile 花最多力 气的地方。VM 里的凭据都是诱饵:当成环境变量的 brm_… 占位串、装着一次性客户端证 书的合成 ~/.kube/config~/.docker/config.json 里假的 base64 blob,以及在宿主 机上重新签名的 AWS 请求——所以任何绕过代理服务器的东西拿到的是 InvalidSignatureException,而不是访问权。SSH 私钥 根本从来不在 VM 里——宿主机通过每个 profile 专属的一个 agent 代签。对某项凭据打开 Require approval to use,每一次 替换就变成宿主机上的一个对话框,带着有时限的授权:五分钟、一小时、这次会话剩下的 时间。文件夹共享上限是你亲手挑的八个目录,所以你 Mac 上其余的部分,根本不在那里让 人读。

而这一切你都看得见。 Security Log 窗口(Window → Security Log…)是宿主机 端一张单一的时序表:包的判定、VM 开过的每一个目的地以及放行还是拒绝、凭据替换、注 入检测。客户机的代码改不了它,因为客户机的代码够不着它。一天结束时,Resources → Storage 底下的 Erase home…Reset to base… 会把 profile 带回刚克隆完的状 态。

把它打开

如果你在跑 NemoClaw,就打上 NVIDIA 的补丁——它把 Ollama 搬回 loopback,并在任何确 实需要对外的东西前面摆一个需要 token 的代理服务器。The Hacker News 指出这个版本并 未完整覆盖 Windows 与 WSL 的路径,所以别靠猜,拿公告去对照你自己的平台。接着去把机 器上其他每一台模型服务器都找出来,问问它们绑在哪个地址上。答案是 0.0.0.0 的次数 比人们预期的多,因为容器一直在要这件事。

在 profile 里,值得花两分钟的设置就是那份老样子的短清单。Guardrails → Outbound connections,把 Unmatched traffic 设成 Deny。Credentials → Require approval to use,用在任何能花钱或删数据的东西上。Supply Chain → OSV vulnerability check,有密钥的话再加上 socket.dev 或 Delpi 过滤,叠在本来就 开着的年龄门槛上。Prompt Injection → 两个检测器都开,让通往你本地模型的流量,被读 得跟通往云端模型的一样仔细。

继续在自己的机器上跑模型吧。那个选择替你买到留在本地的提示词,值得拥有。但它对于 还有谁能跟抱着这些提示词的服务器说话什么也没说,而那是一个关于 socket 的问题,而 socket 值得一个比“为了哄容器开心而设的环境变量”更好的答案。 装上 Bromure Agentic Coding,在宿主机上挑你的模型,留给客户机一 根管子,而不是一个端口。