GitHub Security Advisory: GHSA-vvjh-f6p9-5vcf
Package: openclaw (NPM)
Severity: HIGH (CVSS 7.4)
CWE: CWE-291: Reliance on IP Address for Authentication
Vulnerable versions: < 2026.2.19
Patched in: 2026.2.19

Summary: OpenClaw Canvas Authentication Bypass Vulnerability

Details:
ZDI-CAN-29311: OpenClaw Canvas Authentication Bypass Vulnerability

-- ABSTRACT -------------------------------------

Trend Micro's Zero Day Initiative has identified a vulnerability affecting the following products:
OpenClaw - OpenClaw

-- VULNERABILITY DETAILS ------------------------
* Version tested: openclaw 2026.2.17
* Platform tested: macOS 26.3

---

### Analysis

## Description

The OpenClaw gateway's `authorizeCanvasRequest()` function implements an IP-based authentication fallback for canvas endpoints (`/__openclaw__/a2ui/`, `/__openclaw__/canvas/`, `/__openclaw__/ws`). When a WebSocket client authenticates from a private IP address, ALL subsequent HTTP requests from that same IP are granted canvas access without requiring their own authentication token.

In environments where multiple clients share a single IP address ��� corporate NAT, VPN concentrators, Kubernetes clusters, Docker host-mode networking ��� an unauthenticated attacker on the same network is granted full canvas access by virtue of sharing an IP with a legitimate authenticated client.

## Root Cause

Three functions in `src/gateway/server-http.ts` create this vulnerability:

### 1. IP-matching function (line ~100)

```typescript
function hasAuthorizedWsClientForIp(clients: Set<GatewayWsClient>, clientIp: string): boolean {
  for (const client of clients) {
    if (client.clientIp && client.clientIp === clientIp) {
      return true;
    }
  }
  return false;
}
```

This function checks if ANY connected WebSocket client shares the same IP. It does not verify that the HTTP request belongs to the same user, session, or browser as the WS client.

### 2. IP-based fallback in authorizeCanvasRequest (line ~109)

```typescript
async function authorizeCanvasRequest(params: { ... }): Promise<GatewayAuthResult> {
  // ... token check first ...

  const clientIp = resolveGatewayClientIp({ ... });

  // Only allow fallback for private/loopback addresses
  if (!isPrivateOrLoopbackAddress(clientIp)) {
    return lastAuthFailure ?? { ok: false, reason: "unauthorized" };
  }

  // THE VULNERABILITY: grants access based on IP alone
  if (hasAuthorizedWsClientForIp(clients, clientIp)) {
    return { ok: true };
  }

  return lastAuthFailure ?? { ok: false, reason: "unauthorized" };
}
```

If the HTTP request comes from a private IP that matches any authenticated WS client, access is granted without verifying the request's own credentials.

### 3. Canvas path routing

```typescript
function isCanvasPath(pathname: string): boolean {
  return (
    pathname === A2UI_PATH ||              // /__openclaw__/a2ui
    pathname.startsWith(`${A2UI_PATH}/`) ||
    pathname === CANVAS_HOST_PATH ||        // /__openclaw__/canvas
    pathname.startsWith(`${CANVAS_HOST_PATH}/`) ||
    pathname === CANVAS_WS_PATH            // /__openclaw__/ws
  );
}
```

All canvas endpoints use this weaker authentication path instead of the standard `authorizeGatewayConnect()` which requires a valid token.

## Attack S