VYPR
Moderate severityNVD Advisory· Published Mar 21, 2026· Updated Mar 25, 2026

OpenClaw < 2026.2.25 - Authentication Bypass via Control UI client.id Parameter

CVE-2026-32057

Description

OpenClaw versions prior to 2026.2.25 contain an authentication bypass vulnerability in the trusted-proxy Control UI pairing mechanism that accepts client.id=control-ui without proper device identity verification. An authenticated node role websocket client can exploit this by using the control-ui client identifier to skip pairing requirements and gain unauthorized access to node event execution flows.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.252026.2.25

Affected products

1

Patches

1
ec45c317f5d0

fix(gateway): block trusted-proxy control-ui node bypass

https://github.com/openclaw/openclawPeter SteinbergerFeb 26, 2026via ghsa
2 files changed · +107 3
  • src/gateway/server.auth.test.ts+105 3 modified
    @@ -131,10 +131,11 @@ async function expectHelloOkServerVersion(port: number, expectedVersion: string)
     }
     
     async function createSignedDevice(params: {
    -  token: string;
    +  token?: string | null;
       scopes: string[];
       clientId: string;
       clientMode: string;
    +  role?: "operator" | "node";
       identityPath?: string;
       nonce: string;
       signedAtMs?: number;
    @@ -149,10 +150,10 @@ async function createSignedDevice(params: {
         deviceId: identity.deviceId,
         clientId: params.clientId,
         clientMode: params.clientMode,
    -    role: "operator",
    +    role: params.role ?? "operator",
         scopes: params.scopes,
         signedAtMs,
    -    token: params.token,
    +    token: params.token ?? null,
         nonce: params.nonce,
       });
       return {
    @@ -187,6 +188,23 @@ async function approvePendingPairingIfNeeded() {
       }
     }
     
    +async function configureTrustedProxyControlUiAuth() {
    +  testState.gatewayAuth = {
    +    mode: "trusted-proxy",
    +    trustedProxy: {
    +      userHeader: "x-forwarded-user",
    +      requiredHeaders: ["x-forwarded-proto"],
    +    },
    +  };
    +  const { writeConfigFile } = await import("../config/config.js");
    +  await writeConfigFile({
    +    gateway: {
    +      trustedProxies: ["127.0.0.1"],
    +    },
    +    // oxlint-disable-next-line typescript/no-explicit-any
    +  } as any);
    +}
    +
     function isConnectResMessage(id: string) {
       return (o: unknown) => {
         if (!o || typeof o !== "object" || Array.isArray(o)) {
    @@ -776,6 +794,90 @@ describe("gateway server auth/connect", () => {
         });
       });
     
    +  test("allows trusted-proxy control ui operator without device identity", async () => {
    +    await configureTrustedProxyControlUiAuth();
    +    await withGatewayServer(async ({ port }) => {
    +      const ws = await openWs(port, {
    +        origin: "https://localhost",
    +        "x-forwarded-for": "203.0.113.10",
    +        "x-forwarded-proto": "https",
    +        "x-forwarded-user": "peter@example.com",
    +      });
    +      const res = await connectReq(ws, {
    +        skipDefaultAuth: true,
    +        role: "operator",
    +        device: null,
    +        client: { ...CONTROL_UI_CLIENT },
    +      });
    +      expect(res.ok).toBe(true);
    +      const status = await rpcReq(ws, "status");
    +      expect(status.ok).toBe(false);
    +      expect(status.error?.message ?? "").toContain("missing scope");
    +      const health = await rpcReq(ws, "health");
    +      expect(health.ok).toBe(true);
    +      ws.close();
    +    });
    +  });
    +
    +  test("rejects trusted-proxy control ui node role without device identity", async () => {
    +    await configureTrustedProxyControlUiAuth();
    +    await withGatewayServer(async ({ port }) => {
    +      const ws = await openWs(port, {
    +        origin: "https://localhost",
    +        "x-forwarded-for": "203.0.113.10",
    +        "x-forwarded-proto": "https",
    +        "x-forwarded-user": "peter@example.com",
    +      });
    +      const res = await connectReq(ws, {
    +        skipDefaultAuth: true,
    +        role: "node",
    +        device: null,
    +        client: { ...CONTROL_UI_CLIENT },
    +      });
    +      expect(res.ok).toBe(false);
    +      expect(res.error?.message ?? "").toContain("control ui requires device identity");
    +      expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
    +        ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
    +      );
    +      ws.close();
    +    });
    +  });
    +
    +  test("requires pairing for trusted-proxy control ui node role with unpaired device", async () => {
    +    await configureTrustedProxyControlUiAuth();
    +    await withGatewayServer(async ({ port }) => {
    +      const ws = await openWs(port, {
    +        origin: "https://localhost",
    +        "x-forwarded-for": "203.0.113.10",
    +        "x-forwarded-proto": "https",
    +        "x-forwarded-user": "peter@example.com",
    +      });
    +      const challengeNonce = await readConnectChallengeNonce(ws);
    +      expect(challengeNonce).toBeTruthy();
    +      const { device } = await createSignedDevice({
    +        token: null,
    +        role: "node",
    +        scopes: [],
    +        clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI,
    +        clientMode: GATEWAY_CLIENT_MODES.WEBCHAT,
    +        nonce: String(challengeNonce),
    +      });
    +      const res = await connectReq(ws, {
    +        skipDefaultAuth: true,
    +        role: "node",
    +        scopes: [],
    +        device,
    +        client: { ...CONTROL_UI_CLIENT },
    +      });
    +      expect(res.ok).toBe(false);
    +      expect(res.error?.message ?? "").toContain("pairing required");
    +      expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
    +        ConnectErrorDetailCodes.PAIRING_REQUIRED,
    +      );
    +      ws.close();
    +    });
    +  });
    +
       test("allows localhost control ui without device identity when insecure auth is enabled", async () => {
         testState.gatewayControlUi = { allowInsecureAuth: true };
         const { server, ws, prevToken } = await startServerWithClient("secret", {
    
  • src/gateway/server/ws-connection/message-handler.ts+2 0 modified
    @@ -491,6 +491,7 @@ export function attachGatewayWsMessageHandler(params: {
               }
               const trustedProxyAuthOk =
                 isControlUi &&
    +            role === "operator" &&
                 resolvedAuth.mode === "trusted-proxy" &&
                 authOk &&
                 authMethod === "trusted-proxy";
    @@ -629,6 +630,7 @@ export function attachGatewayWsMessageHandler(params: {
     
             const trustedProxyAuthOk =
               isControlUi &&
    +          role === "operator" &&
               resolvedAuth.mode === "trusted-proxy" &&
               authOk &&
               authMethod === "trusted-proxy";
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.