Medium severity6.8GHSA Advisory· Published May 5, 2026· Updated May 7, 2026
CVE-2026-43535
CVE-2026-43535
Description
OpenClaw before 2026.4.14 contains an authorization context reuse vulnerability in collect-mode queue batches that allows messages from different senders to inherit the final sender's authorization context. Attackers can exploit this by sending multiple queued messages to drain batches using a more privileged sender's context, causing earlier messages to execute with elevated permissions.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.14 | 2026.4.14 |
Affected products
2Patches
143d4be902755fix(queue): split collect batches by auth context (#66024)
3 files changed · +853 −19
CHANGELOG.md+1 −0 modified@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - Cron/scheduler: stop inventing short retries when cron next-run calculation returns no valid future slot, and keep a maintenance wake armed so enabled unscheduled jobs recover without entering a refire loop. (#66019, #66083) Thanks @mbelinky. - Cron/scheduler: preserve the active error-backoff floor when maintenance repair recomputes a missing cron next-run, so recurring errored jobs do not resume early after a transient next-run resolution failure. (#66019, #66083, #66113) Thanks @mbelinky. - Outbound/delivery-queue: persist the originating outbound `session` context on queued delivery entries and replay it during recovery, so write-ahead-queued sends keep their original outbound media policy context after restart instead of evaluating against a missing session. (#66025) Thanks @eleqtrizit. +- Auto-reply/queue: split collect-mode followup drains into contiguous groups by per-message authorization context (sender id, owner status, exec/bash-elevated overrides), so queued items from different senders or exec configs no longer execute under the last queued run's owner-only and exec-approval context. (#66024) Thanks @eleqtrizit. ## 2026.4.12
src/auto-reply/reply/queue.collect.test.ts+747 −0 modified@@ -6,6 +6,7 @@ import { createQueueTestRun as createRun, installQueueRuntimeErrorSilencer, } from "./queue.test-helpers.js"; +import { resolveFollowupAuthorizationKey } from "./queue/drain.js"; installQueueRuntimeErrorSilencer(); @@ -94,6 +95,440 @@ describe("followup queue collect routing", () => { expect(calls[0]?.originatingTo).toBe("channel:A"); }); + it("splits collect batches when sender authorization changes", async () => { + const key = `test-collect-auth-split-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const expectedCalls = 2; + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + if (calls.length >= expectedCalls) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const nonOwner = createRun({ + prompt: "use the gateway tool", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + enqueueFollowupRun( + key, + { + ...nonOwner, + run: { + ...nonOwner.run, + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + const owner = createRun({ + prompt: "what's the weather?", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + enqueueFollowupRun( + key, + { + ...owner, + run: { + ...owner.run, + senderId: "owner-1", + senderName: "Owner", + senderIsOwner: true, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls.map((call) => call.run.senderIsOwner)).toEqual([false, true]); + expect(calls[0]?.prompt).toContain("use the gateway tool"); + expect(calls[0]?.prompt).not.toContain("what's the weather?"); + expect(calls[1]?.prompt).toContain("what's the weather?"); + expect(calls[1]?.prompt).toContain("(from Owner)"); + }); + + it("keeps one collect batch when authorization context matches", async () => { + const key = `test-collect-auth-match-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + done.resolve(); + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const first = createRun({ + prompt: "first", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const second = createRun({ + prompt: "second", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + + enqueueFollowupRun( + key, + { + ...first, + run: { + ...first.run, + senderId: "user-1", + senderName: "Guest", + senderUsername: "guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...second, + run: { + ...second.run, + senderId: "user-1", + senderName: "Guest", + senderUsername: "guest", + senderIsOwner: false, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls).toHaveLength(1); + expect(calls[0]?.prompt).toContain("first"); + expect(calls[0]?.prompt).toContain("second"); + expect(calls[0]?.prompt).toContain("(from Guest)"); + }); + + it("keeps one collect batch when only sender display fields drift", async () => { + const key = `test-collect-auth-display-drift-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + done.resolve(); + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const first = createRun({ + prompt: "first", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const second = createRun({ + prompt: "second", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + + enqueueFollowupRun( + key, + { + ...first, + run: { + ...first.run, + senderId: "user-1", + senderName: "Guest", + senderUsername: "guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...second, + run: { + ...second.run, + senderId: "user-1", + senderName: "Guest User", + senderUsername: "guest-renamed", + senderIsOwner: false, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls).toHaveLength(1); + expect(calls[0]?.prompt).toContain("first"); + expect(calls[0]?.prompt).toContain("second"); + expect(calls[0]?.prompt).toContain("(from Guest)"); + expect(calls[0]?.prompt).toContain("(from Guest User)"); + }); + + it("splits collect batches when exec context changes", async () => { + const key = `test-collect-exec-split-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const expectedCalls = 2; + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + if (calls.length >= expectedCalls) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const base = createRun({ + prompt: "first", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + + enqueueFollowupRun( + key, + { + ...base, + run: { + ...base.run, + senderId: "owner-1", + senderIsOwner: true, + bashElevated: { enabled: false, allowed: true, defaultLevel: "off" }, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...createRun({ + prompt: "second", + originatingChannel: "slack", + originatingTo: "channel:A", + }), + run: { + ...base.run, + senderId: "owner-1", + senderIsOwner: true, + bashElevated: { enabled: true, allowed: true, defaultLevel: "on" }, + execOverrides: { ask: "always" }, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls[0]?.prompt).toContain("first"); + expect(calls[0]?.prompt).not.toContain("second"); + expect(calls[1]?.prompt).toContain("second"); + expect(calls[1]?.run.bashElevated?.enabled).toBe(true); + expect(calls[1]?.run.execOverrides?.ask).toBe("always"); + }); + + it("uses the newest run within a matching authorization batch", async () => { + const key = `test-collect-latest-run-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + done.resolve(); + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const first = createRun({ prompt: "first", originatingChannel: "slack", originatingTo: "A" }); + const second = createRun({ + prompt: "second", + originatingChannel: "slack", + originatingTo: "A", + }); + + enqueueFollowupRun( + key, + { + ...first, + run: { + ...first.run, + provider: "openai", + model: "gpt-5.4", + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...second, + run: { + ...second.run, + provider: "anthropic", + model: "sonnet-4.6", + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls).toHaveLength(1); + expect(calls[0]?.run.provider).toBe("anthropic"); + expect(calls[0]?.run.model).toBe("sonnet-4.6"); + }); + + it("delivers and clears summary-only collect drains after cross-channel items", async () => { + const key = `test-collect-summary-only-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const expectedCalls = 3; + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + if (calls.length >= expectedCalls) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 2, + dropPolicy: "summarize", + }; + + enqueueFollowupRun( + key, + createRun({ + prompt: "first", + originatingChannel: "slack", + originatingTo: "channel:A", + }), + settings, + ); + enqueueFollowupRun( + key, + createRun({ + prompt: "second", + originatingChannel: "slack", + originatingTo: "channel:B", + }), + settings, + ); + enqueueFollowupRun( + key, + createRun({ + prompt: "third", + originatingChannel: "slack", + originatingTo: "channel:C", + }), + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls).toHaveLength(3); + expect(calls[0]?.prompt).toBe("second"); + expect(calls[1]?.prompt).toBe("third"); + expect(calls[2]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); + expect(calls[2]?.prompt).toContain("- first"); + }); + + it("preserves collect order when authorization changes more than once", async () => { + const key = `test-collect-auth-order-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const expectedCalls = 3; + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + if (calls.length >= expectedCalls) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const first = createRun({ prompt: "first", originatingChannel: "slack", originatingTo: "A" }); + const second = createRun({ prompt: "second", originatingChannel: "slack", originatingTo: "A" }); + const third = createRun({ prompt: "third", originatingChannel: "slack", originatingTo: "A" }); + + enqueueFollowupRun( + key, + { + ...first, + run: { ...first.run, senderId: "user-a", senderName: "A", senderIsOwner: false }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...second, + run: { ...second.run, senderId: "owner-1", senderName: "Owner", senderIsOwner: true }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...third, + run: { ...third.run, senderId: "user-a", senderName: "A", senderIsOwner: false }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls.map((call) => call.prompt)).toEqual([ + expect.stringContaining("first"), + expect.stringContaining("second"), + expect.stringContaining("third"), + ]); + }); + it("collects Slack messages in same thread and preserves string thread id", async () => { const key = `test-collect-slack-thread-same-${Date.now()}`; const calls: FollowupRun[] = []; @@ -212,6 +647,83 @@ describe("followup queue collect routing", () => { expect(calls[0]?.prompt).toContain("Queued #2\ntwo"); }); + it("retries only the remaining collect auth groups after a partial failure", async () => { + const key = `test-collect-partial-retry-${Date.now()}`; + const attempts: FollowupRun[] = []; + const successfulCalls: FollowupRun[] = []; + const done = createDeferred<void>(); + let attempt = 0; + const runFollowup = async (run: FollowupRun) => { + attempt += 1; + attempts.push(run); + if (attempt === 2) { + throw new Error("transient failure"); + } + successfulCalls.push(run); + if (attempt >= 3) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 50, + dropPolicy: "summarize", + }; + + const guest = createRun({ + prompt: "guest message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const owner = createRun({ + prompt: "owner message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + + enqueueFollowupRun( + key, + { + ...guest, + run: { + ...guest.run, + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...owner, + run: { + ...owner.run, + senderId: "owner-1", + senderName: "Owner", + senderIsOwner: true, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + const guestAttempts = attempts.filter((call) => call.prompt.includes("guest message")); + const ownerAttempts = attempts.filter((call) => call.prompt.includes("owner message")); + + expect(attempts).toHaveLength(3); + expect(guestAttempts).toHaveLength(1); + expect(ownerAttempts).toHaveLength(2); + expect(successfulCalls.map((call) => call.prompt)).toEqual([ + expect.stringContaining("guest message"), + expect.stringContaining("owner message"), + ]); + }); + it("retries overflow summary delivery without losing dropped previews", async () => { const key = `test-overflow-summary-retry-${Date.now()}`; const calls: FollowupRun[] = []; @@ -241,6 +753,183 @@ describe("followup queue collect routing", () => { expect(calls[0]?.prompt).toContain("- first"); }); + it("includes the overflow summary only in the first split auth group", async () => { + const key = `test-collect-overflow-summary-once-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + const expectedCalls = 2; + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + if (calls.length >= expectedCalls) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 2, + dropPolicy: "summarize", + }; + + const droppedGuest = createRun({ + prompt: "dropped guest message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const guest = createRun({ + prompt: "guest message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const owner = createRun({ + prompt: "owner message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + + enqueueFollowupRun( + key, + { + ...droppedGuest, + run: { + ...droppedGuest.run, + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...guest, + run: { + ...guest.run, + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...owner, + run: { + ...owner.run, + senderId: "owner-1", + senderName: "Owner", + senderIsOwner: true, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls).toHaveLength(2); + expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); + expect(calls[0]?.prompt).toContain("- dropped guest message"); + expect(calls[1]?.prompt).not.toContain("[Queue overflow]"); + expect(calls[1]?.prompt).not.toContain("dropped guest message"); + }); + + it("does not re-deliver overflow summary on partial auth group failure retry", async () => { + const key = `test-collect-overflow-partial-retry-${Date.now()}`; + const calls: FollowupRun[] = []; + const done = createDeferred<void>(); + let attempt = 0; + const runFollowup = async (run: FollowupRun) => { + attempt += 1; + // First group succeeds (attempt 1), second group fails (attempt 2), + // then second group succeeds on retry (attempt 3). + if (attempt === 2) { + throw new Error("transient failure"); + } + calls.push(run); + if (calls.length >= 2) { + done.resolve(); + } + }; + const settings: QueueSettings = { + mode: "collect", + debounceMs: 0, + cap: 2, + dropPolicy: "summarize", + }; + + const droppedGuest = createRun({ + prompt: "dropped guest message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const guest = createRun({ + prompt: "guest message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + const owner = createRun({ + prompt: "owner message", + originatingChannel: "slack", + originatingTo: "channel:A", + }); + + enqueueFollowupRun( + key, + { + ...droppedGuest, + run: { + ...droppedGuest.run, + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...guest, + run: { + ...guest.run, + senderId: "user-1", + senderName: "Guest", + senderIsOwner: false, + }, + }, + settings, + ); + enqueueFollowupRun( + key, + { + ...owner, + run: { + ...owner.run, + senderId: "owner-1", + senderName: "Owner", + senderIsOwner: true, + }, + }, + settings, + ); + + scheduleFollowupDrain(key, runFollowup); + await done.promise; + + expect(calls).toHaveLength(2); + // First group got the overflow summary + expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); + expect(calls[0]?.prompt).toContain("- dropped guest message"); + // Second group (retried after failure) must NOT get the overflow summary again + expect(calls[1]?.prompt).not.toContain("[Queue overflow]"); + expect(calls[1]?.prompt).not.toContain("dropped guest message"); + expect(calls[1]?.prompt).toContain("owner message"); + }); + it("preserves routing metadata on overflow summary followups", async () => { const key = `test-overflow-summary-routing-${Date.now()}`; const calls: FollowupRun[] = []; @@ -289,3 +978,61 @@ describe("followup queue collect routing", () => { expect(calls[0]?.prompt).toContain("[Queue overflow] Dropped 1 message due to cap."); }); }); + +describe("resolveFollowupAuthorizationKey", () => { + it("changes when sender ownership changes", () => { + const run = createRun({ prompt: "one" }).run; + expect( + resolveFollowupAuthorizationKey({ + ...run, + senderId: "user-1", + senderIsOwner: false, + }), + ).not.toBe( + resolveFollowupAuthorizationKey({ + ...run, + senderId: "user-1", + senderIsOwner: true, + }), + ); + }); + + it("changes when exec defaults change", () => { + const run = createRun({ prompt: "one" }).run; + expect( + resolveFollowupAuthorizationKey({ + ...run, + senderId: "user-1", + bashElevated: { enabled: false, allowed: true, defaultLevel: "off" }, + }), + ).not.toBe( + resolveFollowupAuthorizationKey({ + ...run, + senderId: "user-1", + bashElevated: { enabled: true, allowed: true, defaultLevel: "on" }, + execOverrides: { ask: "always" }, + }), + ); + }); + + it("does not change when only sender display fields change", () => { + const run = createRun({ prompt: "one" }).run; + expect( + resolveFollowupAuthorizationKey({ + ...run, + senderId: "user-1", + senderName: "Guest", + senderUsername: "guest", + senderIsOwner: false, + }), + ).toBe( + resolveFollowupAuthorizationKey({ + ...run, + senderId: "user-1", + senderName: "Guest User", + senderUsername: "guest-renamed", + senderIsOwner: false, + }), + ); + }); +});
src/auto-reply/reply/queue/drain.ts+105 −19 modified@@ -59,6 +59,63 @@ function resolveOriginRoutingMetadata(items: FollowupRun[]): OriginRoutingMetada }; } +// Keep this key aligned with the fields that affect per-message authorization or +// exec-context propagation in collect-mode batching. Display-only sender fields +// stay out of the key so profile/name drift does not force conservative splits. +// Fields like authProfileId, elevatedLevel, ownerNumbers, and config are +// intentionally excluded because they are session-level or not consulted in +// per-message authorization checks. +export function resolveFollowupAuthorizationKey(run: FollowupRun["run"]): string { + return JSON.stringify([ + run.senderId ?? "", + run.senderE164 ?? "", + run.senderIsOwner === true, + run.execOverrides?.host ?? "", + run.execOverrides?.security ?? "", + run.execOverrides?.ask ?? "", + run.execOverrides?.node ?? "", + run.bashElevated?.enabled === true, + run.bashElevated?.allowed === true, + run.bashElevated?.defaultLevel ?? "", + ]); +} + +function splitCollectItemsByAuthorization(items: FollowupRun[]): FollowupRun[][] { + if (items.length <= 1) { + return items.length === 0 ? [] : [items]; + } + + const groups: FollowupRun[][] = []; + let currentGroup: FollowupRun[] = []; + let currentKey: string | undefined; + + for (const item of items) { + const itemKey = resolveFollowupAuthorizationKey(item.run); + if (currentGroup.length === 0 || itemKey === currentKey) { + currentGroup.push(item); + currentKey = itemKey; + continue; + } + + groups.push(currentGroup); + currentGroup = [item]; + currentKey = itemKey; + } + + if (currentGroup.length > 0) { + groups.push(currentGroup); + } + + return groups; +} + +function renderCollectItem(item: FollowupRun, idx: number): string { + const senderLabel = + item.run.senderName ?? item.run.senderUsername ?? item.run.senderId ?? item.run.senderE164; + const senderSuffix = senderLabel ? ` (from ${senderLabel})` : ""; + return `---\nQueued #${idx + 1}${senderSuffix}\n${item.prompt}`.trim(); +} + function resolveCrossChannelKey(item: FollowupRun): { cross?: true; key?: string } { const { originatingChannel: channel, originatingTo: to, originatingAccountId: accountId } = item; const threadId = item.originatingThreadId; @@ -108,6 +165,17 @@ export function scheduleFollowupDrain( run: effectiveRunFollowup, }); if (collectDrainResult === "empty") { + const summaryOnlyPrompt = previewQueueSummaryPrompt({ state: queue, noun: "message" }); + const run = queue.lastRun; + if (summaryOnlyPrompt && run) { + await effectiveRunFollowup({ + prompt: summaryOnlyPrompt, + run, + enqueuedAt: Date.now(), + }); + clearQueueSummaryState(queue); + continue; + } break; } if (collectDrainResult === "drained") { @@ -116,28 +184,46 @@ export function scheduleFollowupDrain( const items = queue.items.slice(); const summary = previewQueueSummaryPrompt({ state: queue, noun: "message" }); - const run = items.at(-1)?.run ?? queue.lastRun; - if (!run) { - break; + const authGroups = splitCollectItemsByAuthorization(items); + if (authGroups.length === 0) { + const run = queue.lastRun; + if (!summary || !run) { + break; + } + await effectiveRunFollowup({ + prompt: summary, + run, + enqueuedAt: Date.now(), + }); + clearQueueSummaryState(queue); + continue; } - const routing = resolveOriginRoutingMetadata(items); + let pendingSummary = summary; + for (const groupItems of authGroups) { + const run = groupItems.at(-1)?.run ?? queue.lastRun; + if (!run) { + break; + } - const prompt = buildCollectPrompt({ - title: "[Queued messages while agent was busy]", - items, - summary, - renderItem: (item, idx) => `---\nQueued #${idx + 1}\n${item.prompt}`.trim(), - }); - await effectiveRunFollowup({ - prompt, - run, - enqueuedAt: Date.now(), - ...routing, - }); - queue.items.splice(0, items.length); - if (summary) { - clearQueueSummaryState(queue); + const routing = resolveOriginRoutingMetadata(groupItems); + const prompt = buildCollectPrompt({ + title: "[Queued messages while agent was busy]", + items: groupItems, + summary: pendingSummary, + renderItem: renderCollectItem, + }); + await effectiveRunFollowup({ + prompt, + run, + enqueuedAt: Date.now(), + ...routing, + }); + queue.items.splice(0, groupItems.length); + if (pendingSummary) { + clearQueueSummaryState(queue); + pendingSummary = undefined; + } } continue; }
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/43d4be902755c970b3d15608679761877718da69nvdPatchWEB
- github.com/advisories/GHSA-jwrq-8g5x-5fhmghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-jwrq-8g5x-5fhmnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-43535ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-authorization-context-reuse-in-collect-mode-queue-batchesnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/pull/66024ghsaWEB
News mentions
0No linked articles in our index yet.