mirror of https://github.com/openclaw/openclaw.git
fix(voice-call): require Twilio signature in ngrok loopback mode
This commit is contained in:
parent
571c195c54
commit
ff11d8793b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -222,7 +222,39 @@ 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": signature,
|
||||
},
|
||||
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(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";
|
||||
|
||||
|
|
@ -243,9 +275,9 @@ describe("verifyTwilioWebhook", () => {
|
|||
{ allowNgrokFreeTierLoopbackBypass: true },
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(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", () => {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue