2026 年初,一类新的攻击面随着 AI 助手的普及浮出水面。OpenClaw(前身 Moltbot 与 ClawdBot)是运行在用户本机的开源 AI 助手,能够代替持有者收发消息、调用外部服务、执行本地命令。它拿到的权限越高,出错的代价越大。

本文研究的对象是 CVE-2026-25253,一个位于 OpenClaw Control UI(控制界面)前端的逻辑缺陷。攻击者诱导受害者访问一个网页,即可在毫秒级时间内窃取网关令牌,随后关闭安全机制并执行任意命令。这条链被研究者称为一键 RCE(1-Click Remote Code Execution)。

本文在写作前对原始分析报告、官方通告、补丁提交、包管理记录与第三方分析做了逐项交叉核实,发现原始材料与官方口径之间存在三处出入,其中两处直接影响受影响范围的判定。正文将如实呈现这些出入,而非照抄任何一方。

一、事实钉死:编号、评分与时间线

1.1 编号与评分

漏洞编号为 CVE-2026-25253,由 MITRE 作为 CNA(CVE Numbering Authority,CVE 编号授权机构)分配。NVD 页面显示 NVD 自身未给出评分,仅转载 CNA 提交的 CVSS v3.1 结果:

Base Score: 8.8 HIGH
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE (MITRE/NVD): CWE-669 Incorrect Resource Transfer Between Spheres

1.2 完整时间线

把各方的公开记录按时间排开,整个事件的节奏相当紧凑:

时间(UTC)

事件

来源

2026-01-25 15:21

npm 包 clawdbot 发布 2026.1.24-3,成为该包名发布的末个版本

npm registry

2026-01-27 00:11

ethiack 的 PoC 仓库 moltbot-1click-rce 创建

GitHub API

2026-01-28 21:32

修复提交 a7534dc 合入,基于 0xacb 的 PR #2880

GitHub API

2026-01-29 11:08

npm 包 openclaw 创建

npm registry

2026-01-30 04:49

版本 2026.1.29 发布,首个修复版

npm registry

2026-01-31

GitHub Advisory GHSA-g8p2-7wf7-98mq 发布

GitHub Advisory

2026-02-01

NVD 收录 CVE-2026-25253,depthfirst 公开分析文章

NVD 变更历史

2026-02-02

NVD 补充 ethiack 分析与 0xacb 的推文引用

NVD 变更历史

2026-02-02 23:41

GHSA 记录被标记为 github_reviewed

OSV / CIRCL

2026-02-03

SecurityWeek 完成首次公开披露

多伦多大学通告

2026-02-04 00:00

第二次修复提交 66d8117 合入,补充 origin 校验

GitHub API

2026-02-04 00:56

版本 2026.2.2 发布,距提交合入仅 56 分钟

npm registry

2026-02-13

NIST 完成初始分析,补充 CPE 配置

NVD 变更历史

2026-06-17

CISA-ADP 补充 SSVC 判定

NVD 变更历史

从补丁合入到公开披露只有五天,从首个修复版到 origin 校验落地隔了六天。这个节奏决定了后文关于"最低安全版本"的判断。

关于 2026-01-27 那个 PoC 仓库,需要说明一点:GitHub API 记录的 created_at 是仓库创建时间,不必然等于代码公开时间,私有仓库转为公开时该字段不会变化。因此只能确认它早于修复版发布,不能据此断言存在无补丁的公开窗口期。

二、影响范围:三个开关

判定一个 OpenClaw 部署是否处于这条攻击链的覆盖之下,不能只看版本号。三个条件各自独立,需要分开确认。

2.1 开关一:版本与包名

受影响区间是全部低于 2026.1.29 的版本,从 0 开始。这一口径由三方共同确认:

来源

受影响区间表述

NVD CPE

cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*

,up to (excluding) 2026.1.29

MITRE affected

pkg:npm/clawdbot

,version 0,lessThan 2026.1.29,status affected

GitHub Advisory

Affected versions <= v2026.1.28,Patched versions v2026.1.29

这里需要指出原始分析报告与官方口径的第一处出入。depthfirst 的文章写的是"All versions up to v2026.1.24-1 are vulnerable"。按这个说法,2026.1.24-2 与 2026.1.24-3 会被误判为安全。而 npm 记录显示,clawdbot 包发布的末尾两个版本恰好就是 2026.1.24-2 与 2026.1.24-3。照原文执行排查,会漏掉旧包名用户中的相当一部分。

第二处出入更为根本。官方标注的修复版本 2026.1.29 只解决了自动连接这一个问题,WebSocket origin 校验是六天后才补上的。本文据此给出两个不同的下界:

  • 阻断一键利用的最低版本:2026.1.29

  • 同时关闭 CSWSH(Cross-Site WebSocket Hijacking,跨站 WebSocket 劫持)利用面的最低版本:2026.2.2

第三处出入涉及 CWE 定级,三方给出了三个不同的编号。MITRE 与 NVD 使用 CWE-669(跨信任域的资源传递不当),GitHub Advisory 页面标注 CWE-200(敏感信息暴露给未授权主体),OSV 与 CIRCL 的记录里写作 CWE-668。三个编号指向的是同一个缺陷的不同侧面,本文采用 NVD 口径,同时标注分歧存在。引用 CWE 编号的文章应当意识到这一点,避免把单一编号当作定论。

2.2 开关二:部署形态

默认部署下,OpenClaw 的网关监听在本机回环地址,Control UI 通过 WebSocket 与之通信:

Control UI 路径: /chat
默认网关地址: ws://127.0.0.1:18789/
认证凭据: token 与(或)password,存于浏览器 localStorage
设备身份: Ed25519 密钥对
消息格式: JSON-RPC 风格,type 取 req / res / event

直觉上,只监听 127.0.0.1 的服务不受远程攻击影响。在这个漏洞上,这个直觉是错的。GitHub Advisory 的原文写明:

原因在于连接的发起方向被反转了。攻击者的服务器无法主动连到受害者的 127.0.0.1,但受害者的浏览器可以。浏览器运行在受害者本机,它发起的到 127.0.0.1:18789 的连接属于本机内部通信,不受任何网络边界设备约束。攻击者只需要让受害者的浏览器执行一段 JavaScript,就能借用这个位置。

换句话说,回环绑定防的是外部主机连进来,防不住本机浏览器连出去。企业环境里常见的"服务只听内网地址所以安全"这类判断,在这个场景里失效。

公网暴露的实例处境更差。攻击者拿到令牌后可以直接从自己的服务器登录,连浏览器跳板这一步都不需要。

2.3 开关三:浏览器是否持有令牌

这条攻击链的起点是窃取令牌,而令牌存放在浏览器的 localStorage 里。这意味着受害者必须曾经在这台机器的这个浏览器里登录过 Control UI。

具体判定条件是:

一,受害者在该浏览器中登录过 OpenClaw Control UI,且未主动退出或清除;
二,攻击者诱导受害者访问了攻击者可控的页面;
三,受害者访问恶意页面时,Control UI 所在的浏览器进程处于可访问状态。

未登录过 Control UI 的会话,攻击者拿不到任何有效凭据,整条链不成立。这也是 PR:N 这个字段容易被误读的地方:它描述的是攻击者不需要账号,不是受害者不需要身份。受害者的身份恰恰是这条链能够成立的必要条件。

2.4 四种不受影响的情形

把三个开关组合起来,可以确定四种明确不在覆盖范围内的情形:

  • 版本不低于 2026.2.2 的部署,两级修复均已生效

  • 从未在该浏览器登录过 Control UI 的机器

  • 仅在隔离浏览器中打开 Control UI,且日常浏览使用独立配置文件的用户

  • 服务未运行且浏览器无残留凭据的机器

反过来,同时满足"版本低于 2026.1.29""浏览器持有令牌""用户会访问外部网页"这三项的部署,处于完整攻击链的覆盖之下,且无法通过回环绑定自行豁免。

三、漏洞点:一处赋值串起的三段代码

漏洞的根因不在任何一个单独的函数里。它由三段各自看起来都合理的代码组合而成,这也是自动化扫描能够发现、人工审读容易漏过的原因。

3.1 第一段:读参数即写设置

ui/src/ui/app-settings.ts 中的 applySettingsFromUrl() 负责解析 URL 查询串,取出 gatewayUrl 参数并写入设置:

const gatewayUrlRaw = params.get("gatewayUrl");
if (gatewayUrlRaw != null) {
  const gatewayU

applySettings() 随后把这个值通过 saveSettings 写入 localStorage,从这一刻起它就成了持久化的配置。

允许通过 URL 参数配置连接目标本身不是缺陷,很多本地工具都这么做,方便用户分享配置或快速切换环境。问题出在这个值被信任的方式上。

3.2 第二段:连接紧跟设置

app-lifecycle.ts 在设置落地之后立刻发起连接:

handleConnected(host) {
  connectGateway(host);
  startNodesPolling(host);
}

connectGateway() 没有任何等待或确认环节。它读取当前设置中的 gatewayUrl,然后建立 WebSocket 连接。把"应用设置"与"建立连接"放在同一个无间断的调用序列里,意味着 URL 参数从被解析到被使用之间不存在任何人工介入的机会。

3.3 第三段:令牌装进握手

gateway.ts 在建立连接时,把本地保存的凭据放进握手参数:

const params = { ..., authToken, locale: navigator.language };
void this.request<GatewayHelloOk>("connect", params);

authToken 是本机的网关令牌,也是整个实例的最高权限凭据。它随着 connect 方法被发送到 gatewayUrl 指向的任意地址。握手包里同时包含设备 ID 与公钥,这是 OpenClaw 设备身份体系的一部分。

3.4 组合之后

三段代码分开看都成立:URL 参数配置是常见设计,设置变更后立即连接是合理的响应逻辑,握手携带凭据是协议要求。缺陷出现在三者的串接方式上,一个未经校验的外部输入,在没有确认环节的情况下,被直接用于决定高价值凭据的发送目的地。

这类缺陷的特征是:没有缓冲区溢出、没有类型混淆、没有越界读写,纯粹是控制流层面的信任传递错误。静态规则难以捕捉,因为它要求的不是"某个函数用错了",而是"三个函数的调用顺序不安全"。depthfirst 的引擎正是通过跨文件的数据流拼接才标记出这个模式。

3.5 关于 CWE 编号的选择

CWE-669 的官方定义是"资源在不正确的信任域之间传递"。用在这里相当贴切:令牌本应只在本机信任域内流转,却因为外部可控的 URL 参数被送到了攻击者的信任域。

CWE-200 侧重"敏感信息暴露给未授权主体",描述的是结果而非机制。CWE-668 侧重"资源暴露在错误的领域",与 669 语义接近。三个编号的差异反映的是定级视角不同,不影响技术判断。


四、从 token 到 RCE:两段式利用链

拿到令牌只是第一步。原始分析报告明确指出,单纯的外泄有三个局限,突破这三个局限才是这条链的真正价值所在。

4.1 直接利用的三种局限

攻击者把 gatewayUrl 指向自己的服务器,收到令牌后登录受害者实例,可以做的事包括读取消息内容、读取业务系统密钥、以受害者身份执行操作。这个层面已经造成实质损害,但存在三个限制:

一,对只监听 localhost 的实例无效。攻击者拿到令牌也连不进去,因为他的服务器无法访问受害者的回环地址。
二,不绕过任何沙箱。如果目标配置了容器化执行,攻击者的命令运行在容器里。
三,不达成任意代码执行。只能调用既有的业务能力,不能自由执行系统命令。

突破这三个限制,需要再解决两个问题:如何连到 localhost,如何关掉安全机制。

4.2 第一跳:令牌外泄

攻击者构造一个指向受害者本机 Control UI 的链接,携带指向自己的 gatewayUrl 参数。受害者的浏览器加载这个链接后,Control UI 解析参数、写入设置、立即连接,令牌随之送出。

攻击者的服务端只需要一个监听 WebSocket 的服务。连接建立后,握手包里的 authToken 字段即为目标凭据。这一跳不涉及任何浏览器漏洞,也不触发任何同源策略检查,因为发起连接的正是受害者自己的浏览器。

整个过程在页面加载后毫秒级完成,用户没有可感知的停顿,也没有任何弹窗。

4.3 第二跳:用浏览器打通 localhost

拿到令牌之后,攻击者仍然连不上受害者的 127.0.0.1。这时需要用到 WebSocket 与 HTTP 在同源策略(Same Origin Policy,SOP)上的差异。

浏览器对 HTTP 请求严格执行同源策略,attacker.com 的脚本无法读取 localhost:18789 的响应。但同源策略不适用于 WebSocket 连接。WebSocket 协议的设计把 origin 校验的责任交给了服务端:服务端必须自己检查请求头里的 Origin,自主决定是否接受连接。

OpenClaw 的 WebSocket 服务端在受影响版本中没有做这项校验,接受来自任何站点的连接。于是攻击者可以在自己的页面里执行一段脚本,直接向受害者本机的网关发起连接:

const ws = new WebSocket("ws://127.0.0.1:18789");

浏览器作为跳板,把攻击者的脚本与受害者本机的服务连接起来。这就是 CSWSH 的形态。攻击者此时既持有合法令牌,又具备到本机网关的连通性,两个条件同时满足。

4.4 关掉安全机制

OpenClaw 内置了两道防护:执行审批与容器沙箱。执行审批由 exec-approvals.json 控制,默认情况下运行危险命令前会弹出提示等待用户确认。沙箱通过配置项把 shell 工具放到容器内执行。

两道防护都通过 API 管理。窃取的令牌携带 operator.admin 与 operator.approvals 两个作用域,攻击者不需要去挖沙箱实现的漏洞,直接调用 API 把它们关掉即可:

{
  "method": "exec.approvals.set",
  "params": { "defaults": { "security": "full", "ask": "off" } }
}

第一条请求把审批模式设为关闭。第二条请求修改执行宿主:

{
  "method": "config.patch",
  "params": { "tools.exec.host": "gateway" }
}

把 tools.exec.host 从容器改为 gateway,命令就会直接运行在宿主机上。

这里暴露出一个架构层面的问题:安全机制的控制接口与业务接口共用同一套鉴权体系,持有高权限令牌的一方可以同时关闭安全机制。审批与沙箱防的是代理行为失控,防不住持有管理员凭据的人主动关闭它们。

4.5 落地执行

两道防护关闭后,攻击者通过 node.invoke 发起命令执行请求,协议结构如下:

{
  "type": "req",
  "id": "4",
  "method": "node.invoke",
  "params": {
    "nodeId": "main",
    "command": "system.run",
    "params": { "cmd": "bash -c 'echo hacked > /tmp/hacked'" },
    "timeoutMs": 60000,
    "idempotencyKey": "rev1"
  }
}

system.run 携带的命令以运行 OpenClaw 进程的用户权限执行。在默认安装形态下,这就是持有者的日常账户权限,可以访问其全部文件、凭据与已登录的会话。

从受害者点击链接到命令执行完成,全过程无感。受害者不需要输入任何内容,也不会看到任何确认提示。

4.6 为什么同源策略挡不住

需要澄清一个常见误解:同源策略并未失效,它只是不适用于 WebSocket。

浏览器在发起 WebSocket 请求时,会在请求头中携带 Origin 字段标明来源,但不会阻止连接建立,也不会因此拒绝后续通信。是否接受这个来源,完全由服务端决定。这是 RFC 6455 的设计选择,目的是允许跨域的实时通信服务正常工作。

代价是服务端必须自己实现校验。任何遗漏这项校验的 WebSocket 服务,都会向整个互联网上的任意网页开放。OpenClaw 在受影响版本中遗漏了这一步,同源策略在这里不构成任何保护。


五、补丁分析:两次提交,两个层面

这个漏洞的修复分两次完成,落在不同版本,切断的是攻击链的不同环节。把两次提交分开看,才能得到准确的升级建议。

5.1 第一次修复:把自动改成确认

提交 a7534dc,作者 Tyler Yust,合入时间 2026-01-28T21:32:10Z,提交信息为"fix(ui): gateway URL confirmation modal (based on #2880) (#3578)",共同作者为 0xacb。

核心改动只有一行,位于 ui/src/ui/app-settings.ts

@@ -33,6 +33,7 @@ type SettingsHost = {
   themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
+ pendingGatewayUrl?: string | null;
 };
 
@@ -98,7 +99,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
   if (gatewayUrlRaw != null) {
     const gatewayUrl = gatewayUrlRaw.trim();
     if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {
- applySettings(host, { ...host.settings, gatewayUrl });
+ host.pendingGatewayUrl = gatewayUrl;
     }
     params.delete("gatewayUrl");
     shouldCleanUrl = true;

applySettings() 的直接调用被替换为把值暂存到 pendingGatewayUrl。设置不再立即生效,连接自然也不会立即发起。配套新增了确认组件 ui/src/ui/views/gateway-url-confirmation.ts,并在主组件中加入两个处理方法:

handleGatewayUrlConfirm() {
  const nextGatewayUrl = this.pendingGatewayUrl;
  if (!nextGatewayUrl) return;
  this.pendingGatewayUrl = null;
  applySettingsInternal(this, { ...this.settings, gatewayUrl: nextGatewayUrl });
  this.connect();
}

用户确认后,值才被写入设置并触发连接。取消操作由对应的 handleGatewayUrlCancel() 处理。

这次修复切断的是"无感"这个特性。攻击从一键零交互变成了需要用户在弹窗上点确认。它不修复 origin 校验,也不改变令牌的存储方式。

5.2 第二次修复:origin 校验与反点击劫持

提交 66d8117,作者 steipete,合入时间 2026-02-04T00:00:57Z,提交信息为"fix: harden control ui framing + ws origin",涉及 11 个文件。

新增的 src/gateway/origin-check.ts 实现了完整的来源判定逻辑:

export function checkBrowserOrigin(params: {
  requestHost?: string;
  origin?: string;
  allowedOrigins?: string[];
}): OriginCheckResult {
  const parsedOrigin = parseOrigin(params.origin);
  if (!parsedOrigin) {
    return { ok: false, reason: "origin missing or invalid" };
  }
 
  const allowlist = (params.allowedOrigins ?? [])
    .map((value) => value.trim().toLowerCase())
    .filter(Boolean);
  if (allowlist.includes(parsedOrigin.origin)) {
    return { ok: true };
  }
 
  const requestHost = normalizeHostHeader(params.requestHost);
  if (requestHost && parsedOrigin.host === requestHost) {
    return { ok: true };
  }
 
  const requestHostname = resolveHostName(requestHost);
  if (isLoopbackHost(parsedOrigin.hostname) && isLoopbackHost(requestHostname)) {
    return { ok: true };
  }
 
  return { ok: false, reason: "origin not allowed" };
}

判定顺序是四步:来源缺失或非法直接拒绝;命中 allowedOrigins 白名单放行;来源与请求 Host 头一致放行;双方均为回环地址放行。其余一律拒绝。

回环判定函数覆盖 localhost::1127.0.0.1 以及整个 127. 网段:

function isLoopbackHost(hostname: string): boolean {
  if (!hostname) return false;
  if (hostname === "localhost") return true;
  if (hostname === "::1") return true;
  if (hostname === "127.0.0.1" || hostname.startsWith("127.")) return true;
  return false;
}

同一次提交还加入了反点击劫持响应头,包括 X-Frame-Options: DENY 与 Content-Security-Policy: frame-ancestors 'none',并新增了 gateway.controlUi.allowedOrigins 配置项供反向代理等场景使用。

5.3 两次修复各自切断了什么

两次修复作用于攻击链的不同位置,效果不能互相替代:

版本

提交

切断的环节

残留风险

2026.1.29

a7534dc

URL 参数自动连接,一键变需确认

WebSocket 仍不校验 origin,令牌若通过社工弹窗等方式外泄,CSWSH 跳板依然可用

2026.2.2

66d8117

跨站 WebSocket 连接,浏览器跳板失效

无(该环节)

需要提醒的是,确认弹窗属于用户可绕过的防护。用户在弹窗上点确认,攻击链依然成立,只是从零交互变成社工交互。origin 校验属于服务端强制约束,无法被用户操作绕过。

因此本文给出的建议下界是 2026.2.2,而不是官方 CVE 记录标注的 2026.1.29。处于 2026.1.29 至 2026.2.1 区间的部署,主链已阻断,纵深防御尚未补齐。

七、结束语

CVE-2026-25253 的技术复杂度不高。它没有内存破坏,没有精巧的利用原语,核心只是一处赋值:把外部可控的 URL 参数,在没有确认环节的情况下,用作高价值凭据的发送目的地。三段各自合理的代码串在一起,构成了一条完整的攻击链。

真正值得警惕的是这条链揭示的结构性问题。当本地 AI 助手同时具备三项能力,即持有高权限凭据、通过 API 管理自身安全机制、且控制界面运行在浏览器中时,浏览器中的任意一次点击都可能被放大为宿主机上的代码执行。回环绑定、审批提示、容器沙箱这三道防线,在这条链上分别被浏览器位置、管理员凭据、配置项改写所绕过。

本次核实发现的三处出入,指向的是同一个方法论问题:漏洞信息的引用需要区分来源层级。原始分析报告给的是研究员视角,官方通告给的是厂商视角,包管理记录与提交历史给的是事实视角。三方不一致时,以提交记录与包管理元数据为准,因为它们无法被叙述方式修饰。

具体到处置层面,需要强调两点。其一,clawdbot 包的用户无法通过升级修复,必须切换包名,而这个断口在包管理器上没有任何标识。其二,官方标注的修复版本 2026.1.29 只完成了一半工作,CSWSH 利用面直到 2026.2.2 才关闭,排查时不应以 2026.1.29 为终点。

AI 助手正在成为新的高价值攻击目标,它们聚合了凭据、通道与执行能力,却往往运行在安全边界的内部。对这类资产的防护,不能沿用"内网服务即安全"的旧假设,也不能把安全机制的控制权与业务权限放在同一枚令牌之下。唯有把凭据的作用域收窄、把安全配置与普通配置分离、把浏览器环境与代理环境隔离,才能在保留便利性的同时,把一次误点击的代价控制在可接受范围内。