Medium severity6.5GHSA Advisory· Published May 5, 2026· Updated May 7, 2026
CVE-2026-43574
CVE-2026-43574
Description
OpenClaw before 2026.4.12 contains an improper authorization vulnerability in helper-backed channels where empty resolved approver lists are interpreted as explicit approval authorization. Attackers can resolve pending approvals without proper authorization by exploiting this logic flaw if they know an approval id.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.12 | 2026.4.12 |
Affected products
2Patches
10a105c0900defix(approval-auth): prevent empty approver list from granting explicit approval authorization [AI] (#65714)
6 files changed · +177 −4
CHANGELOG.md+1 −0 modified@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- fix(approval-auth): prevent empty approver list from granting explicit approval authorization [AI]. (#65714) Thanks @pgondhi987. - fix(security): broaden shell-wrapper detection and block env-argv assignment injection [AI-assisted]. (#65717) Thanks @pgondhi987. - Gateway/startup: defer scheduled services until sidecars finish, gate chat history and model listing during sidecar resume, and let Control UI retry startup-gated history loads so Sandbox wake resumes channels first. (#65365) Thanks @lml2468. - Control UI/chat: load the live gateway slash-command catalog into the composer and command palette so dock commands, plugin commands, and direct skill aliases appear in chat, while keeping trusted local commands authoritative and bounding remote command metadata. (#65620) Thanks @BunsDev.
src/auto-reply/reply/commands-approve.test.ts+58 −1 modified@@ -115,7 +115,8 @@ const signalApproveTestPlugin: ChannelPlugin = { approvalCapability: createResolvedApproverActionAuthAdapter({ channelLabel: "Signal", resolveApprovers: ({ cfg, accountId }) => { - const signal = accountId ? cfg.channels?.signal?.accounts?.[accountId] : cfg.channels?.signal; + const scopedSignal = accountId ? cfg.channels?.signal?.accounts?.[accountId] : undefined; + const signal = scopedSignal ?? cfg.channels?.signal; return resolveApprovalApprovers({ allowFrom: signal?.allowFrom, defaultTo: signal?.defaultTo, @@ -643,6 +644,62 @@ describe("handleApproveCommand", () => { expect(callGatewayMock).not.toHaveBeenCalled(); }); + it("does not allow empty helper approvers to bypass unauthorized sender checks", async () => { + const params = buildApproveParams( + "/approve abc12345 allow-once", + { + commands: { text: true }, + channels: { + signal: { + allowFrom: [], + }, + }, + } as OpenClawConfig, + { + Provider: "signal", + Surface: "signal", + SenderId: "+15551239999", + }, + ); + params.command.isAuthorizedSender = false; + + const result = await handleApproveCommand(params, true); + expect(result?.shouldContinue).toBe(false); + expect(result?.reply).toBeUndefined(); + expect(callGatewayMock).not.toHaveBeenCalled(); + }); + + it("keeps same-chat /approve available to authorized senders when helper approvers are empty", async () => { + callGatewayMock.mockResolvedValue({ ok: true }); + const params = buildApproveParams( + "/approve abc12345 allow-once", + { + commands: { text: true }, + channels: { + signal: { + allowFrom: [], + }, + }, + } as OpenClawConfig, + { + Provider: "signal", + Surface: "signal", + SenderId: "+15551239999", + }, + ); + params.command.isAuthorizedSender = true; + + const result = await handleApproveCommand(params, true); + expect(result?.shouldContinue).toBe(false); + expect(result?.reply?.text).toContain("Approval allow-once submitted"); + expect(callGatewayMock).toHaveBeenCalledWith( + expect.objectContaining({ + method: "exec.approval.resolve", + params: { id: "abc12345", decision: "allow-once" }, + }), + ); + }); + it("accepts Telegram /approve from exec target recipients when native approvals are disabled", async () => { const params = buildApproveParams( "/approve abc12345 allow-once",
src/infra/channel-approval-auth.test.ts+39 −0 modified@@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createResolvedApproverActionAuthAdapter } from "../plugin-sdk/approval-auth-helpers.js"; const getChannelPluginMock = vi.hoisted(() => vi.fn()); @@ -116,4 +117,42 @@ describe("resolveApprovalCommandAuthorization", () => { approvalKind: "exec", }); }); + + it("keeps empty approver fallback implicit without bypassing channel sender auth", () => { + getChannelPluginMock.mockReturnValue({ + approvalCapability: createResolvedApproverActionAuthAdapter({ + channelLabel: "Signal", + resolveApprovers: () => [], + }), + }); + + expect( + resolveApprovalCommandAuthorization({ + cfg: {} as never, + channel: "signal", + accountId: "work", + senderId: "uuid:attacker", + kind: "exec", + }), + ).toEqual({ authorized: true, explicit: false }); + }); + + it("keeps configured approvers explicit when sender matches", () => { + getChannelPluginMock.mockReturnValue({ + approvalCapability: createResolvedApproverActionAuthAdapter({ + channelLabel: "Signal", + resolveApprovers: () => ["uuid:owner"], + }), + }); + + expect( + resolveApprovalCommandAuthorization({ + cfg: {} as never, + channel: "signal", + accountId: "work", + senderId: "uuid:owner", + kind: "exec", + }), + ).toEqual({ authorized: true, explicit: true }); + }); });
src/infra/channel-approval-auth.ts+7 −1 modified@@ -1,5 +1,6 @@ import { getChannelPlugin, resolveChannelApprovalCapability } from "../channels/plugins/index.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { isImplicitSameChatApprovalAuthorization } from "../plugin-sdk/approval-auth-helpers.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; export type ApprovalCommandAuthorization = { @@ -30,6 +31,9 @@ export function resolveApprovalCommandAuthorization(params: { if (!resolved) { return { authorized: true, explicit: false }; } + // Keep `resolved` by reference; cloning before this check would drop the + // non-enumerable implicit-fallback marker. + const implicitSameChatAuthorization = isImplicitSameChatApprovalAuthorization(resolved); const availability = approvalCapability?.getActionAvailabilityState?.({ cfg: params.cfg, accountId: params.accountId, @@ -39,6 +43,8 @@ export function resolveApprovalCommandAuthorization(params: { return { authorized: resolved.authorized, reason: resolved.reason, - explicit: resolved.authorized ? availability?.kind !== "disabled" : true, + explicit: resolved.authorized + ? !implicitSameChatAuthorization && availability?.kind !== "disabled" + : true, }; }
src/plugin-sdk/approval-auth-helpers.test.ts+36 −1 modified@@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers.js"; +import { + createResolvedApproverActionAuthAdapter, + isImplicitSameChatApprovalAuthorization, +} from "./approval-auth-helpers.js"; describe("createResolvedApproverActionAuthAdapter", () => { it.each([ @@ -55,4 +58,36 @@ describe("createResolvedApproverActionAuthAdapter", () => { ).toEqual(testCase.expected); } }); + + it("marks empty-approver fallback auth as implicit", () => { + const auth = createResolvedApproverActionAuthAdapter({ + channelLabel: "Signal", + resolveApprovers: () => [], + }); + const result = auth.authorizeActorAction({ + cfg: {}, + senderId: "uuid:attacker", + action: "approve", + approvalKind: "exec", + }); + + expect(result).toEqual({ authorized: true }); + expect(isImplicitSameChatApprovalAuthorization(result)).toBe(true); + }); + + it("does not mark configured-approver auth as implicit", () => { + const auth = createResolvedApproverActionAuthAdapter({ + channelLabel: "Signal", + resolveApprovers: () => ["uuid:owner"], + }); + const result = auth.authorizeActorAction({ + cfg: {}, + senderId: "uuid:owner", + action: "approve", + approvalKind: "exec", + }); + + expect(result).toEqual({ authorized: true }); + expect(isImplicitSameChatApprovalAuthorization(result)).toBe(false); + }); });
src/plugin-sdk/approval-auth-helpers.ts+36 −1 modified@@ -2,6 +2,40 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { OpenClawConfig } from "./config-runtime.js"; type ApprovalKind = "exec" | "plugin"; +type ApprovalAuthorizationResult = { + authorized: boolean; + reason?: string; +}; +const IMPLICIT_SAME_CHAT_APPROVAL_AUTHORIZATION = Symbol( + "openclaw.implicitSameChatApprovalAuthorization", +); + +function markImplicitSameChatApprovalAuthorization( + result: ApprovalAuthorizationResult, +): ApprovalAuthorizationResult { + // Keep this non-enumerable to avoid changing auth payload shape. + // Consumers must pass the same object reference to + // `isImplicitSameChatApprovalAuthorization`; spread/Object.assign/JSON clones + // drop this marker. + Object.defineProperty(result, IMPLICIT_SAME_CHAT_APPROVAL_AUTHORIZATION, { + value: true, + enumerable: false, + }); + return result; +} + +export function isImplicitSameChatApprovalAuthorization( + result: ApprovalAuthorizationResult | null | undefined, +): boolean { + return Boolean( + result && + ( + result as ApprovalAuthorizationResult & { + [IMPLICIT_SAME_CHAT_APPROVAL_AUTHORIZATION]?: true; + } + )[IMPLICIT_SAME_CHAT_APPROVAL_AUTHORIZATION], + ); +} export function createResolvedApproverActionAuthAdapter(params: { channelLabel: string; @@ -25,7 +59,8 @@ export function createResolvedApproverActionAuthAdapter(params: { }) { const approvers = params.resolveApprovers({ cfg, accountId }); if (approvers.length === 0) { - return { authorized: true } as const; + // Empty approver sets are implicit same-chat fallback, not explicit approver bypass. + return markImplicitSameChatApprovalAuthorization({ authorized: true }); } const normalizedSenderId = senderId ? normalizeSenderId(senderId) : undefined; if (normalizedSenderId && approvers.includes(normalizedSenderId)) {
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- github.com/openclaw/openclaw/commit/0a105c0900de701d2ee9f1abc96b017afbd0afddnvdPatchWEB
- github.com/advisories/GHSA-49cg-279w-m73xghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-49cg-279w-m73xnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-43574ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-improper-authorization-via-empty-approver-listsnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/pull/65714ghsaWEB
News mentions
9- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 27th April – Threat Intelligence ReportCheck Point Research · Apr 27, 2026
- Agents that remember: introducing Agent MemoryCloudflare Blog · Apr 17, 2026
- The Increasing Role of AI in Vulnerability ResearchWordfence Blog · Apr 10, 2026
- 16th March – Threat Intelligence ReportCheck Point Research · Mar 16, 2026
- How AI Assistants are Moving the Security GoalpostsKrebs on Security · Mar 8, 2026
- Risky Business #827 -- Iranian cyber threat actors are down but not outRisky Business · Mar 4, 2026
- Risky Business #826 -- A week of AI mishaps and skulduggeryRisky Business · Feb 25, 2026