VYPR
High severity7.1NVD Advisory· Published Apr 21, 2026· Updated Apr 27, 2026

CVE-2026-41299

CVE-2026-41299

Description

OpenClaw before 2026.3.28 contains an authorization bypass vulnerability in the chat.send gateway method where ACP-only provenance fields are gated by self-declared client metadata from WebSocket handshake rather than verified authorization state. Authenticated operator clients can spoof ACP identity labels and inject reserved provenance fields intended only for the ACP bridge by manipulating client metadata during connection.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

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

Patches

1
4b9542716c26

Gateway: require verified scope for chat provenance (#55700)

https://github.com/openclaw/openclawJacob TomlinsonMar 27, 2026via ghsa
2 files changed · +103 6
  • src/gateway/server-methods/chat.directive-tags.test.ts+93 1 modified
    @@ -1110,11 +1110,102 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
         const [ok, _payload, error] = respond.mock.calls.at(-1) ?? [];
         expect(ok).toBe(false);
         expect(error).toMatchObject({
    -      message: "system provenance fields are reserved for the ACP bridge",
    +      message: "system provenance fields require admin scope",
         });
         expect(mockState.lastDispatchCtx).toBeUndefined();
       });
     
    +  it("rejects forged ACP metadata when the caller lacks admin scope", async () => {
    +    createTranscriptFixture("openclaw-chat-send-system-provenance-spoof-reject-");
    +    mockState.finalText = "ok";
    +    const respond = vi.fn();
    +    const context = createChatContext();
    +
    +    await runNonStreamingChatSend({
    +      context,
    +      respond,
    +      idempotencyKey: "idem-system-provenance-spoof-reject",
    +      client: {
    +        connect: {
    +          scopes: ["operator.write"],
    +          client: {
    +            id: "cli",
    +            mode: "cli",
    +            displayName: "ACP",
    +            version: "acp",
    +          },
    +        },
    +      },
    +      requestParams: {
    +        systemInputProvenance: {
    +          kind: "external_user",
    +          originSessionId: "acp-session-spoof",
    +          sourceChannel: "acp",
    +          sourceTool: "openclaw_acp",
    +        },
    +        systemProvenanceReceipt:
    +          "[Source Receipt]\nbridge=openclaw-acp\noriginSessionId=acp-session-spoof\n[/Source Receipt]",
    +      },
    +      expectBroadcast: false,
    +      waitForCompletion: false,
    +    });
    +
    +    const [ok, _payload, error] = respond.mock.calls.at(-1) ?? [];
    +    expect(ok).toBe(false);
    +    expect(error).toMatchObject({
    +      message: "system provenance fields require admin scope",
    +    });
    +    expect(mockState.lastDispatchCtx).toBeUndefined();
    +  });
    +
    +  it("allows admin-scoped clients to inject system provenance without ACP metadata", async () => {
    +    createTranscriptFixture("openclaw-chat-send-system-provenance-admin-");
    +    mockState.finalText = "ok";
    +    const respond = vi.fn();
    +    const context = createChatContext();
    +
    +    await runNonStreamingChatSend({
    +      context,
    +      respond,
    +      idempotencyKey: "idem-system-provenance-admin",
    +      message: "ops update",
    +      client: {
    +        connect: {
    +          scopes: ["operator.admin"],
    +          client: {
    +            id: "custom-operator",
    +            mode: "cli",
    +            displayName: "custom-operator",
    +            version: "1.0.0",
    +          },
    +        },
    +      },
    +      requestParams: {
    +        systemInputProvenance: {
    +          kind: "external_user",
    +          originSessionId: "admin-session-1",
    +          sourceChannel: "acp",
    +          sourceTool: "openclaw_acp",
    +        },
    +        systemProvenanceReceipt:
    +          "[Source Receipt]\nbridge=openclaw-acp\noriginSessionId=admin-session-1\n[/Source Receipt]",
    +      },
    +      expectBroadcast: false,
    +    });
    +
    +    expect(mockState.lastDispatchCtx?.InputProvenance).toEqual({
    +      kind: "external_user",
    +      originSessionId: "admin-session-1",
    +      sourceChannel: "acp",
    +      sourceTool: "openclaw_acp",
    +    });
    +    expect(mockState.lastDispatchCtx?.Body).toBe(
    +      "[Source Receipt]\nbridge=openclaw-acp\noriginSessionId=admin-session-1\n[/Source Receipt]\n\nops update",
    +    );
    +    expect(mockState.lastDispatchCtx?.RawBody).toBe("ops update");
    +    expect(mockState.lastDispatchCtx?.CommandBody).toBe("ops update");
    +  });
    +
       it("injects ACP system provenance into the agent-visible body", async () => {
         createTranscriptFixture("openclaw-chat-send-system-provenance-acp-");
         mockState.finalText = "ok";
    @@ -1128,6 +1219,7 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
           message: "bench update",
           client: {
             connect: {
    +          scopes: ["operator.admin"],
               client: {
                 id: "cli",
                 mode: "cli",
    
  • src/gateway/server-methods/chat.ts+10 5 modified
    @@ -299,6 +299,11 @@ function isAcpBridgeClient(client: GatewayRequestHandlerOptions["client"]): bool
       );
     }
     
    +function canInjectSystemProvenance(client: GatewayRequestHandlerOptions["client"]): boolean {
    +  const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : [];
    +  return scopes.includes(ADMIN_SCOPE);
    +}
    +
     async function persistChatSendImages(params: {
       images: ChatImageContent[];
       client: GatewayRequestHandlerOptions["client"];
    @@ -1265,14 +1270,14 @@ export const chatHandlers: GatewayRequestHandlers = {
           systemProvenanceReceipt?: string;
           idempotencyKey: string;
         };
    -    if ((p.systemInputProvenance || p.systemProvenanceReceipt) && !isAcpBridgeClient(client)) {
    +    if (
    +      (p.systemInputProvenance || p.systemProvenanceReceipt) &&
    +      !canInjectSystemProvenance(client)
    +    ) {
           respond(
             false,
             undefined,
    -        errorShape(
    -          ErrorCodes.INVALID_REQUEST,
    -          "system provenance fields are reserved for the ACP bridge",
    -        ),
    +        errorShape(ErrorCodes.INVALID_REQUEST, "system provenance fields require admin scope"),
           );
           return;
         }
    

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

4

News mentions

0

No linked articles in our index yet.