VYPR
Moderate severityNVD Advisory· Published Mar 5, 2026· Updated Mar 9, 2026

OpenClaw < 2026.2.14 - Webhook Signature Verification Bypass via ngrok Loopback Compatibility

CVE-2026-29606

Description

OpenClaw versions prior to 2026.2.14 contain a webhook signature-verification bypass in the voice-call extension that allows unauthenticated requests when the tunnel.allowNgrokFreeTierLoopbackBypass option is explicitly enabled. An external attacker can send forged requests to the publicly reachable webhook endpoint without a valid X-Twilio-Signature header, resulting in unauthorized webhook event handling and potential request flooding attacks.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.142026.2.14

Affected products

1

Patches

1
ff11d8793b90

fix(voice-call): require Twilio signature in ngrok loopback mode

https://github.com/openclaw/openclawPeter SteinbergerFeb 14, 2026via ghsa
4 files changed · +47 18
  • CHANGELOG.md+1 0 modified
    @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
     - Security/Discovery: stop treating Bonjour TXT records as authoritative routing (prefer resolved service endpoints) and prevent discovery from overriding stored TLS pins; autoconnect now requires a previously trusted gateway. Thanks @simecek.
     - macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins.
     - Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
    +- Security/Voice Call: require valid Twilio webhook signatures even when ngrok free tier loopback compatibility mode is enabled. Thanks @p80n-sec.
     - Security/Google Chat: deprecate `users/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
     - Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc.
     - Security/Browser: block cross-origin mutating requests to loopback browser control routes (CSRF hardening). Thanks @vincentkoc.
    
  • extensions/voice-call/src/config.ts+4 2 modified
    @@ -207,8 +207,10 @@ export const VoiceCallTunnelConfigSchema = z
         ngrokDomain: z.string().min(1).optional(),
         /**
          * Allow ngrok free tier compatibility mode.
    -     * When true, signature verification failures on ngrok-free.app URLs
    -     * will be allowed only for loopback requests (ngrok local agent).
    +     * When true, forwarded headers may be trusted for loopback requests
    +     * to reconstruct the public ngrok URL used for signing.
    +     *
    +     * IMPORTANT: This does NOT bypass signature verification.
          */
         allowNgrokFreeTierLoopbackBypass: z.boolean().default(false),
       })
    
  • extensions/voice-call/src/webhook-security.test.ts+35 3 modified
    @@ -222,17 +222,24 @@ describe("verifyTwilioWebhook", () => {
         expect(result.reason).toMatch(/Invalid signature/);
       });
     
    -  it("allows invalid signatures for ngrok free tier only on loopback", () => {
    +  it("accepts valid signatures for ngrok free tier on loopback when compatibility mode is enabled", () => {
         const authToken = "test-auth-token";
         const postBody = "CallSid=CS123&CallStatus=completed&From=%2B15550000000";
    +    const webhookUrl = "https://local.ngrok-free.app/voice/webhook";
    +
    +    const signature = twilioSignature({
    +      authToken,
    +      url: webhookUrl,
    +      postBody,
    +    });
     
         const result = verifyTwilioWebhook(
           {
             headers: {
               host: "127.0.0.1:3334",
               "x-forwarded-proto": "https",
               "x-forwarded-host": "local.ngrok-free.app",
    -          "x-twilio-signature": "invalid",
    +          "x-twilio-signature": signature,
             },
             rawBody: postBody,
             url: "http://127.0.0.1:3334/voice/webhook",
    @@ -244,8 +251,33 @@ describe("verifyTwilioWebhook", () => {
         );
     
         expect(result.ok).toBe(true);
    +    expect(result.verificationUrl).toBe(webhookUrl);
    +  });
    +
    +  it("does not allow invalid signatures for ngrok free tier on loopback", () => {
    +    const authToken = "test-auth-token";
    +    const postBody = "CallSid=CS123&CallStatus=completed&From=%2B15550000000";
    +
    +    const result = verifyTwilioWebhook(
    +      {
    +        headers: {
    +          host: "127.0.0.1:3334",
    +          "x-forwarded-proto": "https",
    +          "x-forwarded-host": "local.ngrok-free.app",
    +          "x-twilio-signature": "invalid",
    +        },
    +        rawBody: postBody,
    +        url: "http://127.0.0.1:3334/voice/webhook",
    +        method: "POST",
    +        remoteAddress: "127.0.0.1",
    +      },
    +      authToken,
    +      { allowNgrokFreeTierLoopbackBypass: true },
    +    );
    +
    +    expect(result.ok).toBe(false);
    +    expect(result.reason).toMatch(/Invalid signature/);
         expect(result.isNgrokFreeTier).toBe(true);
    -    expect(result.reason).toMatch(/compatibility mode/);
       });
     
       it("ignores attacker X-Forwarded-Host without allowedHosts or trustForwardingHeaders", () => {
    
  • extensions/voice-call/src/webhook-security.ts+7 13 modified
    @@ -339,7 +339,13 @@ export function verifyTwilioWebhook(
       options?: {
         /** Override the public URL (e.g., from config) */
         publicUrl?: string;
    -    /** Allow ngrok free tier compatibility mode (loopback only, less secure) */
    +    /**
    +     * Allow ngrok free tier compatibility mode (loopback only).
    +     *
    +     * IMPORTANT: This does NOT bypass signature verification.
    +     * It only enables trusting forwarded headers on loopback so we can
    +     * reconstruct the public ngrok URL that Twilio used for signing.
    +     */
         allowNgrokFreeTierLoopbackBypass?: boolean;
         /** Skip verification entirely (only for development) */
         skipVerification?: boolean;
    @@ -401,18 +407,6 @@ export function verifyTwilioWebhook(
       const isNgrokFreeTier =
         verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
     
    -  if (isNgrokFreeTier && options?.allowNgrokFreeTierLoopbackBypass && isLoopback) {
    -    console.warn(
    -      "[voice-call] Twilio signature validation failed (ngrok free tier compatibility, loopback only)",
    -    );
    -    return {
    -      ok: true,
    -      reason: "ngrok free tier compatibility mode (loopback only)",
    -      verificationUrl,
    -      isNgrokFreeTier: true,
    -    };
    -  }
    -
       return {
         ok: false,
         reason: `Invalid signature for URL: ${verificationUrl}`,
    

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.