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

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

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

Patches

1
85777e726cb0

Voice Call: canonicalize Plivo V3 replay key (#56003)

https://github.com/openclaw/openclawJacob TomlinsonMar 27, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.