VYPR
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.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.312026.3.31

Affected products

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

Patches

1
4d038bb242c1

fix(zalo): scope webhook replay dedupe per target (#58196)

https://github.com/openclaw/openclawVincent KocMar 31, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.