Medium severity4.2NVD Advisory· Published Apr 28, 2026· Updated Apr 30, 2026
CVE-2026-41402
CVE-2026-41402
Description
OpenClaw before 2026.3.31 contains a scope bypass vulnerability in webhook replay cache deduplication that allows authenticated attackers to replay messages across sibling targets using the same messageId. Attackers can exploit overly broad cache keying to bypass replay protection and deliver duplicate webhook messages to unintended targets.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.31 | 2026.3.31 |
Affected products
1Patches
14d038bb242c1fix(zalo): scope webhook replay dedupe per target (#58196)
3 files changed · +61 −3
CHANGELOG.md+1 −0 modified@@ -130,6 +130,7 @@ Docs: https://docs.openclaw.ai - Diffs/config: preserve schema-shaped plugin config parsing from `diffsPluginConfigSchema.safeParse()`, so direct callers keep `defaults` and `security` sections instead of receiving flattened tool defaults. (#57904) Thanks @gumadeiras. - Diffs: fall back to plain text when `lang` hints are invalid during diff render and viewer hydration, so bad or stale language values no longer break the diff viewer. (#57902) Thanks @gumadeiras. - Doctor/plugins: skip false Matrix legacy-helper warnings when no migration plans exist, and keep bundled `enabledByDefault` plugins in the gateway startup set. (#57931) Thanks @dinakars777. +- Zalo/webhooks: scope replay dedupe to the authenticated target so one configured account can no longer cause same-id inbound events for another target to be dropped. Thanks @smaeljaish771 and @vincentkoc. - Matrix/CLI send: start one-off Matrix send clients before outbound delivery so `openclaw message send --channel matrix` restores E2EE in encrypted rooms instead of sending plain events. (#57936) Thanks @gumadeiras. - xAI/Responses: normalize image-bearing tool results for xAI responses payloads, including OpenResponses-style `input_image.source` parts, so image tool replays no longer 422 on the follow-up turn. (#58017) Thanks @neeravmakwana. - Cron/isolated sessions: carry the full live-session provider, model, and auth-profile selection across retry restarts so cron jobs with model overrides no longer fail or loop on mid-run model-switch requests. (#57972) Thanks @issaba1.
extensions/zalo/src/monitor.webhook.test.ts+56 −0 modified@@ -238,6 +238,62 @@ describe("handleZaloWebhookRequest", () => { } }); + it("keeps replay dedupe isolated per authenticated target", async () => { + const sinkA = vi.fn(); + const sinkB = vi.fn(); + const unregisterA = registerTarget({ + path: "/hook-replay-scope", + secret: "secret-a", + statusSink: sinkA, + }); + const unregisterB = registerTarget({ + path: "/hook-replay-scope", + secret: "secret-b", + statusSink: sinkB, + account: { + ...DEFAULT_ACCOUNT, + accountId: "work", + }, + }); + const payload = createTextUpdate({ + messageId: "msg-replay-scope-1", + userId: "123", + userName: "", + chatId: "123", + text: "hello", + }); + + try { + await withServer(webhookRequestHandler, async (baseUrl) => { + const first = await fetch(`${baseUrl}/hook-replay-scope`, { + method: "POST", + headers: { + "x-bot-api-secret-token": "secret-a", + "content-type": "application/json", + }, + body: JSON.stringify(payload), + }); + const second = await fetch(`${baseUrl}/hook-replay-scope`, { + method: "POST", + headers: { + "x-bot-api-secret-token": "secret-b", + "content-type": "application/json", + }, + body: JSON.stringify(payload), + }); + + expect(first.status).toBe(200); + expect(second.status).toBe(200); + }); + + expect(sinkA).toHaveBeenCalledTimes(1); + expect(sinkB).toHaveBeenCalledTimes(1); + } finally { + unregisterA(); + unregisterB(); + } + }); + it("downloads inbound image media from webhook photo_url and preserves display_name", async () => { const { core,
extensions/zalo/src/monitor.webhook.ts+4 −3 modified@@ -59,6 +59,7 @@ const webhookAnomalyTracker = createWebhookAnomalyTracker({ export function clearZaloWebhookSecurityStateForTest(): void { webhookRateLimiter.clear(); + recentWebhookEvents.clear(); webhookAnomalyTracker.clear(); } @@ -87,12 +88,12 @@ function timingSafeEquals(left: string, right: string): boolean { return timingSafeEqual(leftBuffer, rightBuffer); } -function isReplayEvent(update: ZaloUpdate, nowMs: number): boolean { +function isReplayEvent(target: ZaloWebhookTarget, update: ZaloUpdate, nowMs: number): boolean { const messageId = update.message?.message_id; if (!messageId) { return false; } - const key = `${update.event_name}:${messageId}`; + const key = `${target.path}:${target.account.accountId}:${update.event_name}:${messageId}`; return recentWebhookEvents.check(key, nowMs); } @@ -222,7 +223,7 @@ export async function handleZaloWebhookRequest( return true; } - if (isReplayEvent(update, nowMs)) { + if (isReplayEvent(target, update, nowMs)) { res.statusCode = 200; res.end("ok"); return true;
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/4d038bb242c11f39e45f6a4bde400e5fd42e4ebfnvdPatchWEB
- github.com/advisories/GHSA-hhq4-97c2-p447ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-hhq4-97c2-p447nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41402ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-webhook-replay-cache-cross-target-messageid-scope-bypassnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/releases/tag/v2026.3.31ghsaWEB
News mentions
0No linked articles in our index yet.