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.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.2 | 2026.4.2 |
Affected products
1Patches
1be10ecef770afix(compare): reuse shared secret comparison helper (#58432)
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- github.com/openclaw/openclaw/commit/be10ecef770a4654519869c3641bbb91087c8c7bnvdPatchWEB
- github.com/advisories/GHSA-jj6q-rrrf-h66hghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-jj6q-rrrf-h66hnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41407ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-timing-side-channel-in-shared-secret-comparisonnvdThird Party AdvisoryWEB
News mentions
0No linked articles in our index yet.