High severity7.5NVD Advisory· Published Apr 28, 2026· Updated Apr 30, 2026
CVE-2026-41395
CVE-2026-41395
Description
OpenClaw before 2026.3.28 contains a webhook replay vulnerability in Plivo V3 signature verification that canonicalizes query ordering for signatures but hashes raw URLs for replay detection. Attackers can reorder query parameters to bypass replay cache detection and trigger duplicate voice-call processing with a captured valid signed webhook.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.28 | 2026.3.28 |
Affected products
1Patches
185777e726cb0Voice Call: canonicalize Plivo V3 replay key (#56003)
2 files changed · +67 −1
extensions/voice-call/src/webhook-security.test.ts+47 −0 modified@@ -317,6 +317,53 @@ describe("verifyPlivoWebhook", () => { expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey); expect(second.isReplay).toBe(true); }); + + it("detects V3 replay when query parameters are reordered", () => { + const authToken = "test-auth-token"; + const nonce = "nonce-v3-reorder"; + const postBody = "CallUUID=uuid&CallStatus=in-progress"; + + const urlA = "https://example.com/voice/webhook?flow=answer&callId=abc"; + const urlB = "https://example.com/voice/webhook?callId=abc&flow=answer"; + + const signatureA = plivoV3Signature({ authToken, urlWithQuery: urlA, postBody, nonce }); + const signatureB = plivoV3Signature({ authToken, urlWithQuery: urlB, postBody, nonce }); + expect(signatureA).toBe(signatureB); + + const first = verifyPlivoWebhook( + { + headers: { + host: "example.com", + "x-forwarded-proto": "https", + "x-plivo-signature-v3": signatureA, + "x-plivo-signature-v3-nonce": nonce, + }, + rawBody: postBody, + url: urlA, + method: "POST", + query: { flow: "answer", callId: "abc" }, + }, + authToken, + ); + + const second = verifyPlivoWebhook( + { + headers: { + host: "example.com", + "x-forwarded-proto": "https", + "x-plivo-signature-v3": signatureB, + "x-plivo-signature-v3-nonce": nonce, + }, + rawBody: postBody, + url: urlB, + method: "POST", + query: { callId: "abc", flow: "answer" }, + }, + authToken, + ); + + expectReplayResultPair(first, second); + }); }); describe("verifyTelnyxWebhook", () => {
extensions/voice-call/src/webhook-security.ts+20 −1 modified@@ -728,6 +728,20 @@ function createPlivoV2ReplayKey(url: string, nonce: string): string { return `plivo:v2:${sha256Hex(`${getBaseUrlNoQuery(url)}\n${nonce}`)}`; } +function createPlivoV3ReplayKey(params: { + method: "GET" | "POST"; + url: string; + postParams: PlivoParamMap; + nonce: string; +}): string { + const baseUrl = constructPlivoV3BaseUrl({ + method: params.method, + url: params.url, + postParams: params.postParams, + }); + return `plivo:v3:${sha256Hex(`${baseUrl}\n${params.nonce}`)}`; +} + function timingSafeEqualString(a: string, b: string): boolean { if (a.length !== b.length) { const dummy = Buffer.from(a); @@ -951,7 +965,12 @@ export function verifyPlivoWebhook( reason: "Invalid Plivo V3 signature", }; } - const replayKey = `plivo:v3:${sha256Hex(`${verificationUrl}\n${nonceV3}`)}`; + const replayKey = createPlivoV3ReplayKey({ + method, + url: verificationUrl, + postParams, + nonce: nonceV3, + }); const isReplay = markReplay(plivoReplayCache, replayKey); return { ok: true, version: "v3", verificationUrl, isReplay, verifiedRequestKey: replayKey }; }
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
4- github.com/advisories/GHSA-8689-gm9g-jgr6ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-8689-gm9g-jgr6nvdVendor AdvisoryWEB
- www.vulncheck.com/advisories/openclaw-webhook-replay-via-query-parameter-reordering-in-plivo-v3nvdThird Party Advisory
- github.com/openclaw/openclaw/commit/85777e726cb02c01a911b3ff832ddf4d664d5c94ghsaWEB
News mentions
0No linked articles in our index yet.