VYPR
Medium severity5.4NVD Advisory· Published Apr 23, 2026· Updated Apr 29, 2026

CVE-2026-41356

CVE-2026-41356

Description

OpenClaw before 2026.3.31 fails to terminate active WebSocket sessions when rotating device tokens. Attackers with previously compromised credentials can maintain unauthorized access through existing WebSocket connections after token rotation.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.312026.3.31

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.31

Patches

1
91f7a6b0fd67

fix(gateway): revoke active sessions on token rotation (#57646)

https://github.com/openclaw/openclawVincent KocMar 31, 2026via ghsa
3 files changed · +77 1
  • CHANGELOG.md+1 0 modified
    @@ -105,6 +105,7 @@ Docs: https://docs.openclaw.ai
     - Matrix/delivery recovery: treat Synapse `User not in room` replay failures as permanent during startup recovery so poisoned queued messages move to `failed/` instead of crash-looping Matrix after restart. (#57426) thanks @dlardo.
     - Plugins/facades: guard bundled plugin facade loads with a cache-first sentinel so circular re-entry stops crashing `xai`, `sglang`, and `vllm` during gateway plugin startup. (#57508) Thanks @openperf.
     - Agents/MCP: dispose bundled MCP runtimes after one-shot `openclaw agent --local` runs finish, while preserving bundled MCP state across in-run retries so local JSON runs exit cleanly without restarting stateful MCP tools mid-run.
    +- Gateway/device tokens: disconnect active device sessions after token rotation so newly rotated credentials revoke existing live connections immediately instead of waiting for those sockets to close naturally. Thanks @zsxsoft and @vincentkoc.
     - Gateway/OpenAI HTTP: restore default operator scopes for bearer-authenticated requests that omit `x-openclaw-scopes`, so headless `/v1/chat/completions` and session-history callers work again after the recent method-scope hardening. (#57596) Thanks @openperf.
     - Gateway/attachments: offload large inbound images without leaking `media://` markers into text-only runs, preserve mixed attachment order for model input/transcripts, and fail closed when model image capability cannot be resolved. (#55513) Thanks @Syysean.
     - Agents/subagents: fix interim subagent runtime display so `/subagents list` and `/subagents info` stop inflating short runtimes and show second-level durations correctly. (#57739) Thanks @samzong.
    
  • src/gateway/server-methods/devices.test.ts+73 1 modified
    @@ -2,9 +2,16 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
     import { deviceHandlers } from "./devices.js";
     import type { GatewayRequestHandlerOptions } from "./types.js";
     
    -const { removePairedDeviceMock, revokeDeviceTokenMock } = vi.hoisted(() => ({
    +const {
    +  getPairedDeviceMock,
    +  removePairedDeviceMock,
    +  revokeDeviceTokenMock,
    +  rotateDeviceTokenMock,
    +} = vi.hoisted(() => ({
    +  getPairedDeviceMock: vi.fn(),
       removePairedDeviceMock: vi.fn(),
       revokeDeviceTokenMock: vi.fn(),
    +  rotateDeviceTokenMock: vi.fn(),
     }));
     
     vi.mock("../../infra/device-pairing.js", async () => {
    @@ -13,8 +20,10 @@ vi.mock("../../infra/device-pairing.js", async () => {
       );
       return {
         ...actual,
    +    getPairedDevice: getPairedDeviceMock,
         removePairedDevice: removePairedDeviceMock,
         revokeDeviceToken: revokeDeviceTokenMock,
    +    rotateDeviceToken: rotateDeviceTokenMock,
       };
     });
     
    @@ -101,6 +110,69 @@ describe("deviceHandlers", () => {
         );
       });
     
    +  it("disconnects active clients after rotating a device token", async () => {
    +    getPairedDeviceMock.mockResolvedValue({
    +      deviceId: "device-1",
    +      scopes: ["operator.pairing"],
    +      tokens: {
    +        operator: {
    +          token: "old-token",
    +          role: "operator",
    +          scopes: ["operator.pairing"],
    +          createdAtMs: 123,
    +        },
    +      },
    +    });
    +    rotateDeviceTokenMock.mockResolvedValue({
    +      ok: true,
    +      entry: {
    +        token: "new-token",
    +        role: "operator",
    +        scopes: ["operator.pairing"],
    +        createdAtMs: 456,
    +        rotatedAtMs: 789,
    +      },
    +    });
    +    const opts = createOptions(
    +      "device.token.rotate",
    +      {
    +        deviceId: " device-1 ",
    +        role: " operator ",
    +        scopes: ["operator.pairing"],
    +      },
    +      {
    +        client: {
    +          connect: {
    +            scopes: ["operator.pairing"],
    +          },
    +        } as never,
    +      },
    +    );
    +
    +    await deviceHandlers["device.token.rotate"](opts);
    +    await Promise.resolve();
    +
    +    expect(rotateDeviceTokenMock).toHaveBeenCalledWith({
    +      deviceId: " device-1 ",
    +      role: " operator ",
    +      scopes: ["operator.pairing"],
    +    });
    +    expect(opts.context.disconnectClientsForDevice).toHaveBeenCalledWith("device-1", {
    +      role: "operator",
    +    });
    +    expect(opts.respond).toHaveBeenCalledWith(
    +      true,
    +      {
    +        deviceId: " device-1 ",
    +        role: "operator",
    +        token: "new-token",
    +        scopes: ["operator.pairing"],
    +        rotatedAtMs: 789,
    +      },
    +      undefined,
    +    );
    +  });
    +
       it("does not disconnect clients when token revocation fails", async () => {
         revokeDeviceTokenMock.mockResolvedValue(null);
         const opts = createOptions("device.token.revoke", {
    
  • src/gateway/server-methods/devices.ts+3 0 modified
    @@ -265,6 +265,9 @@ export const deviceHandlers: GatewayRequestHandlers = {
           },
           undefined,
         );
    +    queueMicrotask(() => {
    +      context.disconnectClientsForDevice?.(deviceId.trim(), { role: entry.role });
    +    });
       },
       "device.token.revoke": async ({ params, respond, context }) => {
         if (!validateDeviceTokenRevokeParams(params)) {
    

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

6

News mentions

0

No linked articles in our index yet.