VYPR
Low severity3.7NVD Advisory· Published Apr 28, 2026· Updated Apr 30, 2026

CVE-2026-41407

CVE-2026-41407

Description

OpenClaw before 2026.4.2 contains a timing side channel vulnerability in shared-secret comparison call sites that use early length-mismatch checks instead of fixed-length comparison helpers. Attackers can measure timing differences to leak secret-length information, weakening constant-time handling for shared secrets.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.22026.4.2

Affected products

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

Patches

1
be10ecef770a

fix(compare): reuse shared secret comparison helper (#58432)

https://github.com/openclaw/openclawAgustin RiveraApr 2, 2026via ghsa
8 files changed · +19 68
  • CHANGELOG.md+1 0 modified
    @@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
     - Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. Thanks @vincentkoc.
     - Gateway: prune empty `node-pending-work` state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.
     - Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
    +- Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared `safeEqualSecret` helper and reject empty auth tokens in BlueBubbles. Thanks @eleqtrizit.
     
     ## 2026.4.1-beta.1
     
    
  • extensions/bluebubbles/src/monitor.ts+4 9 modified
    @@ -1,5 +1,5 @@
    -import { timingSafeEqual } from "node:crypto";
     import type { IncomingMessage, ServerResponse } from "node:http";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";
     import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";
     import { logVerbose, processMessage, processReaction } from "./monitor-processing.js";
    @@ -116,18 +116,13 @@ function normalizeAuthToken(raw: string): string {
       return value;
     }
     
    -function safeEqualSecret(aRaw: string, bRaw: string): boolean {
    +function safeEqualAuthToken(aRaw: string, bRaw: string): boolean {
       const a = normalizeAuthToken(aRaw);
       const b = normalizeAuthToken(bRaw);
       if (!a || !b) {
         return false;
       }
    -  const bufA = Buffer.from(a, "utf8");
    -  const bufB = Buffer.from(b, "utf8");
    -  if (bufA.length !== bufB.length) {
    -    return false;
    -  }
    -  return timingSafeEqual(bufA, bufB);
    +  return safeEqualSecret(a, b);
     }
     
     function collectTrustedProxies(targets: readonly WebhookTarget[]): string[] {
    @@ -198,7 +193,7 @@ export async function handleBlueBubblesWebhookRequest(
             res,
             isMatch: (target) => {
               const token = target.account.config.password?.trim() ?? "";
    -          return safeEqualSecret(guid, token);
    +          return safeEqualAuthToken(guid, token);
             },
           });
           if (!target) {
    
  • extensions/feishu/src/monitor.transport.ts+2 10 modified
    @@ -1,6 +1,7 @@
     import * as http from "http";
     import crypto from "node:crypto";
     import * as Lark from "@larksuiteoapi/node-sdk";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import {
       applyBasicWebhookRequestGuards,
       isRequestBodyLimitError,
    @@ -34,15 +35,6 @@ function isFeishuWebhookPayload(value: unknown): value is Record<string, unknown
       return !!value && typeof value === "object" && !Array.isArray(value);
     }
     
    -function timingSafeEqualString(left: string, right: string): boolean {
    -  const leftBuffer = Buffer.from(left, "utf8");
    -  const rightBuffer = Buffer.from(right, "utf8");
    -  if (leftBuffer.length !== rightBuffer.length) {
    -    return false;
    -  }
    -  return crypto.timingSafeEqual(leftBuffer, rightBuffer);
    -}
    -
     function buildFeishuWebhookEnvelope(
       req: http.IncomingMessage,
       payload: Record<string, unknown>,
    @@ -83,7 +75,7 @@ function isFeishuWebhookSignatureValid(params: {
         .createHash("sha256")
         .update(timestamp + nonce + encryptKey + params.rawBody)
         .digest("hex");
    -  return timingSafeEqualString(computedSignature, signature);
    +  return safeEqualSecret(computedSignature, signature);
     }
     
     function respondText(res: http.ServerResponse, statusCode: number, body: string): void {
    
  • extensions/mattermost/src/mattermost/interactions.ts+3 5 modified
    @@ -1,5 +1,6 @@
    -import { createHmac, timingSafeEqual } from "node:crypto";
    +import { createHmac } from "node:crypto";
     import type { IncomingMessage, ServerResponse } from "node:http";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import { getMattermostRuntime } from "../runtime.js";
     import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js";
     import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "./runtime-api.js";
    @@ -223,10 +224,7 @@ export function verifyInteractionToken(
       accountId?: string,
     ): boolean {
       const expected = generateInteractionToken(context, accountId);
    -  if (expected.length !== token.length) {
    -    return false;
    -  }
    -  return timingSafeEqual(Buffer.from(expected), Buffer.from(token));
    +  return safeEqualSecret(expected, token);
     }
     
     // ── Button builder helpers ─────────────────────────────────────────────
    
  • extensions/telegram/src/webhook.ts+2 7 modified
    @@ -1,8 +1,8 @@
    -import { timingSafeEqual } from "node:crypto";
     import { createServer } from "node:http";
     import type { IncomingMessage } from "node:http";
     import net from "node:net";
     import * as grammy from "grammy";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
     import { isDiagnosticsEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
     import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
    @@ -102,12 +102,7 @@ function hasValidTelegramWebhookSecret(
       secretHeader: string | undefined,
       expectedSecret: string,
     ): boolean {
    -  if (typeof secretHeader !== "string") {
    -    return false;
    -  }
    -  const actual = Buffer.from(secretHeader, "utf-8");
    -  const expected = Buffer.from(expectedSecret, "utf-8");
    -  return actual.length === expected.length && timingSafeEqual(actual, expected);
    +  return safeEqualSecret(secretHeader, expectedSecret);
     }
     
     function parseIpLiteral(value: string | undefined): string | undefined {
    
  • extensions/voice-call/src/providers/twilio.ts+2 6 modified
    @@ -1,4 +1,5 @@
     import crypto from "node:crypto";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import type { TwilioConfig, WebhookSecurityConfig } from "../config.js";
     import { getHeader } from "../http-headers.js";
     import type { MediaStreamHandler } from "../media-stream.js";
    @@ -205,12 +206,7 @@ export class TwilioProvider implements VoiceCallProvider {
         if (!expected || !token) {
           return false;
         }
    -    if (expected.length !== token.length) {
    -      const dummy = Buffer.from(expected);
    -      crypto.timingSafeEqual(dummy, dummy);
    -      return false;
    -    }
    -    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(token));
    +    return safeEqualSecret(expected, token);
       }
     
       /**
    
  • extensions/voice-call/src/webhook-security.ts+3 16 modified
    @@ -1,4 +1,5 @@
     import crypto from "node:crypto";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import { getHeader } from "./http-headers.js";
     import type { WebhookContext } from "./types.js";
     
    @@ -120,16 +121,7 @@ function buildCanonicalTwilioParamString(params: URLSearchParams): string {
      * Timing-safe string comparison to prevent timing attacks.
      */
     function timingSafeEqual(a: string, b: string): boolean {
    -  if (a.length !== b.length) {
    -    // Still do comparison to maintain constant time
    -    const dummy = Buffer.from(a);
    -    crypto.timingSafeEqual(dummy, dummy);
    -    return false;
    -  }
    -
    -  const bufA = Buffer.from(a);
    -  const bufB = Buffer.from(b);
    -  return crypto.timingSafeEqual(bufA, bufB);
    +  return safeEqualSecret(a, b);
     }
     
     /**
    @@ -745,12 +737,7 @@ function createPlivoV3ReplayKey(params: {
     }
     
     function timingSafeEqualString(a: string, b: string): boolean {
    -  if (a.length !== b.length) {
    -    const dummy = Buffer.from(a);
    -    crypto.timingSafeEqual(dummy, dummy);
    -    return false;
    -  }
    -  return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
    +  return safeEqualSecret(a, b);
     }
     
     function validatePlivoV2Signature(params: {
    
  • extensions/zalo/src/monitor.webhook.ts+2 15 modified
    @@ -1,5 +1,5 @@
    -import { timingSafeEqual } from "node:crypto";
     import type { IncomingMessage, ServerResponse } from "node:http";
    +import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
     import type { ResolvedZaloAccount } from "./accounts.js";
     import type { ZaloFetch, ZaloUpdate } from "./api.js";
     import type { ZaloRuntimeEnv } from "./monitor.js";
    @@ -72,20 +72,7 @@ export function getZaloWebhookStatusCounterSizeForTest(): number {
     }
     
     function timingSafeEquals(left: string, right: string): boolean {
    -  const leftBuffer = Buffer.from(left);
    -  const rightBuffer = Buffer.from(right);
    -
    -  if (leftBuffer.length !== rightBuffer.length) {
    -    const length = Math.max(1, leftBuffer.length, rightBuffer.length);
    -    const paddedLeft = Buffer.alloc(length);
    -    const paddedRight = Buffer.alloc(length);
    -    leftBuffer.copy(paddedLeft);
    -    rightBuffer.copy(paddedRight);
    -    timingSafeEqual(paddedLeft, paddedRight);
    -    return false;
    -  }
    -
    -  return timingSafeEqual(leftBuffer, rightBuffer);
    +  return safeEqualSecret(left, right);
     }
     
     function isReplayEvent(target: ZaloWebhookTarget, update: ZaloUpdate, nowMs: number): boolean {
    

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

News mentions

0

No linked articles in our index yet.