Skip to content

Commit 094c0d4

Browse files
committed
fix(voice-call): bound Twilio API hosts
1 parent d69c5ba commit 094c0d4

3 files changed

Lines changed: 31 additions & 2 deletions

File tree

extensions/voice-call/src/providers/twilio-region.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,24 @@ const TWILIO_API_HOSTNAME_BY_REGION = {
88
au1: "api.sydney.au1.twilio.com",
99
} satisfies Record<TwilioRegion, string>;
1010

11+
const TWILIO_API_HOSTNAMES = new Set(Object.values(TWILIO_API_HOSTNAME_BY_REGION));
12+
13+
export function resolveTwilioApiHostname(region?: TwilioRegion): string {
14+
return TWILIO_API_HOSTNAME_BY_REGION[region ?? "us1"];
15+
}
16+
1117
export function resolveTwilioApiBaseUrl(params: {
1218
accountSid: string;
1319
region?: TwilioRegion;
1420
}): string {
15-
const hostname = TWILIO_API_HOSTNAME_BY_REGION[params.region ?? "us1"];
21+
const hostname = resolveTwilioApiHostname(params.region);
1622
return `https://${hostname}/2010-04-01/Accounts/${params.accountSid}`;
1723
}
24+
25+
export function requireSupportedTwilioApiHostname(baseUrl: string): string {
26+
const hostname = new URL(baseUrl).hostname;
27+
if (!TWILIO_API_HOSTNAMES.has(hostname)) {
28+
throw new Error(`Unsupported Twilio API hostname: ${hostname}`);
29+
}
30+
return hostname;
31+
}

extensions/voice-call/src/providers/twilio/api.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ describe("twilioApiRequest", () => {
126126
expect(release).toHaveBeenCalledTimes(1);
127127
});
128128

129+
it("rejects unsupported API hosts before the SSRF guard", async () => {
130+
await expect(
131+
twilioApiRequest({
132+
baseUrl: "https://metadata.google.internal/2010-04-01/Accounts/AC123",
133+
accountSid: "AC123",
134+
authToken: "secret",
135+
endpoint: "/Calls.json",
136+
body: {},
137+
}),
138+
).rejects.toThrow("Unsupported Twilio API hostname: metadata.google.internal");
139+
expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled();
140+
});
141+
129142
it("maps AU1 to Twilio's Sydney regional hostname", () => {
130143
expect(resolveTwilioApiBaseUrl({ accountSid: "AC123", region: "au1" })).toBe(
131144
"https://api.sydney.au1.twilio.com/2010-04-01/Accounts/AC123",

extensions/voice-call/src/providers/twilio/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
readProviderErrorResponseSnippet,
66
readProviderJsonResponseText,
77
} from "../shared/response-body.js";
8+
import { requireSupportedTwilioApiHostname } from "../twilio-region.js";
89

910
// Guarded Twilio REST API client helpers.
1011

@@ -74,6 +75,7 @@ export async function twilioApiRequest<T = unknown>(params: {
7475
}, new URLSearchParams());
7576

7677
const requestUrl = `${params.baseUrl}${params.endpoint}`;
78+
const allowedHostname = requireSupportedTwilioApiHostname(params.baseUrl);
7779
const { response, release } = await fetchWithSsrFGuard({
7880
url: requestUrl,
7981
init: {
@@ -84,7 +86,7 @@ export async function twilioApiRequest<T = unknown>(params: {
8486
},
8587
body: bodyParams,
8688
},
87-
policy: { allowedHostnames: [new URL(requestUrl).hostname] },
89+
policy: { allowedHostnames: [allowedHostname] },
8890
timeoutMs: TWILIO_API_TIMEOUT_MS,
8991
auditContext: "voice-call.twilio.api",
9092
});

0 commit comments

Comments
 (0)