VYPR
Medium severity5.3NVD Advisory· Published Apr 23, 2026· Updated Apr 28, 2026

CVE-2026-41351

CVE-2026-41351

Description

OpenClaw before 2026.3.31 contains a replay detection bypass vulnerability in webhook signature handling that treats Base64 and Base64URL encoded signatures as distinct requests. Attackers can re-encode Telnyx webhook signatures to bypass replay detection while maintaining valid signature verification.

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
ad7766605465

fix(voice-call): canonicalize Telnyx replay request keys (#57829)

https://github.com/openclaw/openclawJacob TomlinsonMar 30, 2026via ghsa
2 files changed · +42 1
  • extensions/voice-call/src/webhook-security.test.ts+39 0 modified
    @@ -393,6 +393,45 @@ describe("verifyTelnyxWebhook", () => {
         expectReplayResultPair(first, second);
       });
     
    +  it("treats Base64 and Base64URL signatures as the same replayed request", () => {
    +    const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
    +    const pemPublicKey = publicKey.export({ format: "pem", type: "spki" }).toString();
    +    const timestamp = String(Math.floor(Date.now() / 1000));
    +    const rawBody = JSON.stringify({
    +      data: { event_type: "call.initiated", payload: { call_control_id: "call-1" } },
    +      nonce: crypto.randomUUID(),
    +    });
    +    const signedPayload = `${timestamp}|${rawBody}`;
    +    const signature = crypto.sign(null, Buffer.from(signedPayload), privateKey).toString("base64");
    +    const urlSafeSignature = signature.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
    +    const first = verifyTelnyxWebhook(
    +      {
    +        headers: {
    +          "telnyx-signature-ed25519": signature,
    +          "telnyx-timestamp": timestamp,
    +        },
    +        rawBody,
    +        url: "https://example.com/voice/webhook",
    +        method: "POST" as const,
    +      },
    +      pemPublicKey,
    +    );
    +    const second = verifyTelnyxWebhook(
    +      {
    +        headers: {
    +          "telnyx-signature-ed25519": urlSafeSignature,
    +          "telnyx-timestamp": timestamp,
    +        },
    +        rawBody,
    +        url: "https://example.com/voice/webhook",
    +        method: "POST" as const,
    +      },
    +      pemPublicKey,
    +    );
    +
    +    expectReplayResultPair(first, second);
    +  });
    +
       it("returns a stable request key when verification is skipped", () => {
         const ctx = {
           headers: {},
    
  • extensions/voice-call/src/webhook-security.ts+3 1 modified
    @@ -534,6 +534,8 @@ export function verifyTelnyxWebhook(
       try {
         const signedPayload = `${timestamp}|${ctx.rawBody}`;
         const signatureBuffer = decodeBase64OrBase64Url(signature);
    +    // Canonicalize equivalent Base64/Base64URL encodings before replay hashing.
    +    const canonicalSignature = signatureBuffer.toString("base64");
         const key = importEd25519PublicKey(publicKey);
     
         const isValid = crypto.verify(null, Buffer.from(signedPayload), key, signatureBuffer);
    @@ -548,7 +550,7 @@ export function verifyTelnyxWebhook(
           return { ok: false, reason: "Timestamp too old" };
         }
     
    -    const replayKey = `telnyx:${sha256Hex(`${timestamp}\n${signature}\n${ctx.rawBody}`)}`;
    +    const replayKey = `telnyx:${sha256Hex(`${timestamp}\n${canonicalSignature}\n${ctx.rawBody}`)}`;
         const isReplay = markReplay(telnyxReplayCache, replayKey);
         return { ok: true, isReplay, verifiedRequestKey: replayKey };
       } catch (err) {
    

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.