Critical severity9.9NVD Advisory· Published Apr 21, 2026· Updated Apr 27, 2026
CVE-2026-41329
CVE-2026-41329
Description
OpenClaw before 2026.3.31 contains a sandbox bypass vulnerability allowing attackers to escalate privileges via heartbeat context inheritance and senderIsOwner parameter manipulation. Attackers can exploit improper context validation to bypass sandbox restrictions and achieve unauthorized privilege escalation.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.31 | 2026.3.31 |
Affected products
1Patches
1a30214a62494fix(heartbeat): block owner-only auth inheritance for exec events (#57652)
7 files changed · +37 −4
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. +- Heartbeat/auth: prevent exec-event heartbeat runs from inheriting owner-only tool access from the session delivery target, so node exec output stays on the non-owner tool surface even when the target session belongs to the owner. Thanks @AntAISecurityLab and @vincentkoc. - 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.
src/auto-reply/command-auth.ts+3 −1 modified@@ -578,7 +578,9 @@ export function resolveCommandAuthorization(params: { Array.isArray(ctx.GatewayClientScopes) && ctx.GatewayClientScopes.includes("operator.admin"); const ownerAllowlistConfigured = ownerAllowAll || explicitOwners.length > 0; - const senderIsOwner = senderIsOwnerByIdentity || senderIsOwnerByScope || ownerAllowAll; + const senderIsOwner = ctx.ForceSenderIsOwnerFalse + ? false + : senderIsOwnerByIdentity || senderIsOwnerByScope || ownerAllowAll; const requireOwner = enforceOwner || ownerAllowlistConfigured; const isOwnerForCommands = !requireOwner ? true
src/auto-reply/command-control.test.ts+21 −0 modified@@ -194,6 +194,27 @@ describe("resolveCommandAuthorization", () => { expect(auth.ownerList).toEqual(["123"]); }); + it("suppresses inherited owner status when the context forbids it", () => { + const cfg = { + channels: { telegram: { allowFrom: ["owner-123"] } }, + } as OpenClawConfig; + + const auth = resolveCommandAuthorization({ + ctx: { + Provider: "exec-event", + Surface: "telegram", + OriginatingChannel: "telegram", + From: "owner-123", + To: "owner-123", + ForceSenderIsOwnerFalse: true, + } as MsgContext, + cfg, + commandAuthorized: true, + }); + + expect(auth.senderIsOwner).toBe(false); + }); + it("does not infer a provider from channel allowlists for webchat command contexts", () => { const cfg = { channels: { whatsapp: { allowFrom: ["+15551234567"] } },
src/auto-reply/templating.ts+2 −0 modified@@ -156,6 +156,8 @@ export type MsgContext = { AcpDispatchTailAfterReset?: boolean; /** Gateway client scopes when the message originates from the gateway. */ GatewayClientScopes?: string[]; + /** Trusted system override for contexts that must never inherit owner semantics. */ + ForceSenderIsOwnerFalse?: boolean; /** Thread identifier (Telegram topic id or Matrix thread event id). */ MessageThreadId?: string | number; /** Platform-native channel/conversation id (e.g. Slack DM channel "D…" id). */
src/infra/heartbeat-runner.ghost-reminder.test.ts+3 −2 modified@@ -81,7 +81,7 @@ describe("Ghost reminder bug (issue #13317)", () => { ): Promise<{ result: Awaited<ReturnType<typeof runHeartbeatOnce>>; sendTelegram: ReturnType<typeof vi.fn>; - calledCtx: { Provider?: string; Body?: string } | null; + calledCtx: { Provider?: string; Body?: string; ForceSenderIsOwnerFalse?: boolean } | null; }> => { return runHeartbeatCase({ tmpPrefix, @@ -100,7 +100,7 @@ describe("Ghost reminder bug (issue #13317)", () => { }): Promise<{ result: Awaited<ReturnType<typeof runHeartbeatOnce>>; sendTelegram: ReturnType<typeof vi.fn>; - calledCtx: { Provider?: string; Body?: string } | null; + calledCtx: { Provider?: string; Body?: string; ForceSenderIsOwnerFalse?: boolean } | null; replyCallCount: number; }> => { return withTempHeartbeatSandbox( @@ -228,6 +228,7 @@ describe("Ghost reminder bug (issue #13317)", () => { expect(result.status).toBe("ran"); expect(calledCtx?.Provider).toBe("exec-event"); + expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(true); expect(calledCtx?.Body).toContain("Handle the result internally"); expect(sendTelegram).not.toHaveBeenCalled(); });
src/infra/heartbeat-runner.returns-default-unset.test.ts+6 −1 modified@@ -1354,8 +1354,13 @@ describe("runHeartbeatOnce", () => { }); expect(res.status).toBe("ran"); expect(sendWhatsApp).toHaveBeenCalledTimes(0); - const calledCtx = replySpy.mock.calls[0]?.[0] as { Provider?: string; Body?: string }; + const calledCtx = replySpy.mock.calls[0]?.[0] as { + Provider?: string; + Body?: string; + ForceSenderIsOwnerFalse?: boolean; + }; expect(calledCtx.Provider).toBe("exec-event"); + expect(calledCtx.ForceSenderIsOwnerFalse).toBe(true); expect(calledCtx.Body).toContain("Handle the result internally"); expect(calledCtx.Body).not.toContain("Please relay the command output to the user"); } finally {
src/infra/heartbeat-runner.ts+1 −0 modified@@ -659,6 +659,7 @@ export async function runHeartbeatOnce(opts: { MessageThreadId: delivery.threadId, Provider: hasExecCompletion ? "exec-event" : hasCronEvents ? "cron-event" : "heartbeat", SessionKey: runSessionKey, + ForceSenderIsOwnerFalse: hasExecCompletion, }; if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) { emitHeartbeatEvent({
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- github.com/openclaw/openclaw/commit/a30214a624946fc5c85c9558a27c1580172374fdnvdPatchWEB
- github.com/advisories/GHSA-g5cg-8x5w-7jpmghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-g5cg-8x5w-7jpmnvdVendor AdvisoryWEB
- www.vulncheck.com/advisories/openclaw-sandbox-bypass-via-heartbeat-context-inheritance-and-senderisowner-escalationnvdThird Party Advisory
- github.com/openclaw/openclaw/releases/tag/v2026.3.31ghsaWEB
News mentions
0No linked articles in our index yet.