mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 02:07:26 +00:00
fix(voice-call): require Twilio signature in ngrok loopback mode
This commit is contained in:
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user