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.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.2.14 | 2026.2.14 |
Affected products
1Patches
1ff11d8793b90fix(voice-call): require Twilio signature in ngrok loopback mode
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- github.com/openclaw/openclaw/commit/ff11d8793b90c52f8d84dae3fbb99307da51b5c9ghsapatchWEB
- github.com/advisories/GHSA-c37p-4qqg-3p76ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-c37p-4qqg-3p76ghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-29606ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-webhook-signature-verification-bypass-via-ngrok-loopback-compatibilityghsathird-party-advisoryWEB
- github.com/openclaw/openclaw/releases/tag/v2026.2.14ghsaWEB
News mentions
0No linked articles in our index yet.