沙箱把端口打开了
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 News、 SiliconANGLE 和 CSO 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.1。Origin 和
Host 现在是同一个攻击者选定的主机名,Ollama 把它读成同源。
rebinding 的机制我们上周已经详细写过,就当
成前提吧:你访问的一个网页,变成你机器上某个服务的客户端,大约只要一分钟。
rebinding 是有意思的那条入侵路径,也是更难的那条。一台开在 0.0.0.0 的服务器根
本不需要任何浏览器把戏,就会应答同一网段上的其他每一台设备:访客的笔记本、智能电
视、隔两张桌子那台被拿下的机器。
他们要的不是代码执行
够到 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,取决于你选了三种反应中的哪一种。
现在,假设模型正在对你撒谎
一个架构主张推得够远,就该有人去测它。那就用你喜欢的任何路径,把胜利直接送给攻击 者:一份被下毒的模板、模型厂商的一个坏日子、一次穿过来的提示词注入。你的代理现在 替别人干活,而它的输出读起来依然很好。把那四种载荷一一放进 profile 走一遍。
引导选择,而选择照样被审。 这种载荷最倚仗模型的权威,也撞到最多墙。Bromure 在
代理看到响应之前,就在宿主机的代理服务器上评估每一次包的抓取,横跨 npm、PyPI、
Cargo、RubyGems、Maven、NuGet、Go modules 与 Packagist,而且这个代理服务器不会问
是谁建议了这个包。两天的年龄门槛默认开着,刚发布的仿冒包所需要的东西,大半在这里
就被挡下。打开 OSV 查询、把包过滤指向 socket.dev 或 Delpi、剥掉安装脚本——于是一
项推荐必须熬过四种模型完全没有发言权的意见。VM 里的 .npmrc 与 pip.conf 可以
把这些设置收得更紧。它们没办法放松。
把东西送回家,但线是你的。 外泄需要对外连接。Guardrails →
Outbound connections 是一张 pf 风格的规则表——一个动作、一个协议(tcp、
udp、web、any)、一个主机或 CIDR、一串端口,web 的话再加一串 HTTP 方法
——自上而下匹配,第一条命中的获胜,而 Unmatched traffic 设为 Allow 或 Deny。把
它设成 Deny,列出你工作真正需要的主机,攻击者的端点就不在其中。强制是在宿主机上的
虚拟交换机与 SNI 层执行的,所以客户机对自己路由做的任何事都改不了判定。
把 bug 写进去,然后试着把它发出去。 Guardrails 还为代理够得着的每个服务带了一
个模式:对 GitHub、GitLab、Bitbucket、AWS、Kubernetes、DigitalOcean、容器镜像仓库
以及每个配置过的数据库端点,设 Block destructive 或 Read-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,在宿主机上挑你的模型,留给客户机一 根管子,而不是一个端口。