Voice Call: enforce exact webhook path match
This commit is contained in:
committed by
Peter Steinberger
parent
dde43121c0
commit
3bd0505433
@@ -135,6 +135,43 @@ describe("VoiceCallWebhookServer stale call reaper", () => {
|
||||
});
|
||||
|
||||
describe("VoiceCallWebhookServer replay handling", () => {
|
||||
it("rejects lookalike webhook paths that only match by prefix", async () => {
|
||||
const verifyWebhook = vi.fn(() => ({ ok: true, verifiedRequestKey: "verified:req:prefix" }));
|
||||
const parseWebhookEvent = vi.fn(() => ({ events: [], statusCode: 200 }));
|
||||
const strictProvider: VoiceCallProvider = {
|
||||
...provider,
|
||||
verifyWebhook,
|
||||
parseWebhookEvent,
|
||||
};
|
||||
const { manager } = createManager([]);
|
||||
const config = createConfig({ serve: { port: 0, bind: "127.0.0.1", path: "/voice/webhook" } });
|
||||
const server = new VoiceCallWebhookServer(config, manager, strictProvider);
|
||||
|
||||
try {
|
||||
const baseUrl = await server.start();
|
||||
const address = (
|
||||
server as unknown as { server?: { address?: () => unknown } }
|
||||
).server?.address?.();
|
||||
const requestUrl = new URL(baseUrl);
|
||||
if (address && typeof address === "object" && "port" in address && address.port) {
|
||||
requestUrl.port = String(address.port);
|
||||
}
|
||||
requestUrl.pathname = "/voice/webhook-evil";
|
||||
|
||||
const response = await fetch(requestUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: "CallSid=CA123&SpeechResult=hello",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(verifyWebhook).not.toHaveBeenCalled();
|
||||
expect(parseWebhookEvent).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("acknowledges replayed webhook requests and skips event side effects", async () => {
|
||||
const replayProvider: VoiceCallProvider = {
|
||||
...provider,
|
||||
|
||||
@@ -255,6 +255,25 @@ export class VoiceCallWebhookServer {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeWebhookPathForMatch(pathname: string): string {
|
||||
const trimmed = pathname.trim();
|
||||
if (!trimmed) {
|
||||
return "/";
|
||||
}
|
||||
const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||
if (prefixed === "/") {
|
||||
return prefixed;
|
||||
}
|
||||
return prefixed.endsWith("/") ? prefixed.slice(0, -1) : prefixed;
|
||||
}
|
||||
|
||||
private isWebhookPathMatch(requestPath: string, configuredPath: string): boolean {
|
||||
return (
|
||||
this.normalizeWebhookPathForMatch(requestPath) ===
|
||||
this.normalizeWebhookPathForMatch(configuredPath)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming HTTP request.
|
||||
*/
|
||||
@@ -266,7 +285,7 @@ export class VoiceCallWebhookServer {
|
||||
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
||||
|
||||
// Check path
|
||||
if (!url.pathname.startsWith(webhookPath)) {
|
||||
if (!this.isWebhookPathMatch(url.pathname, webhookPath)) {
|
||||
res.statusCode = 404;
|
||||
res.end("Not Found");
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user