From 70e31c6f68b88bcf91f6df827a0ea05bb90ab112 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 01:47:07 +0100 Subject: [PATCH] fix(gateway): harden hooks URL parsing (#26864) --- .../server-http.hooks-request-timeout.test.ts | 20 +++++++++++++++++-- src/gateway/server-http.ts | 6 ++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/gateway/server-http.hooks-request-timeout.test.ts b/src/gateway/server-http.hooks-request-timeout.test.ts index 448707eb1c7..577ffe1ab43 100644 --- a/src/gateway/server-http.hooks-request-timeout.test.ts +++ b/src/gateway/server-http.hooks-request-timeout.test.ts @@ -41,10 +41,11 @@ function createHooksConfig(): HooksConfigResolved { function createRequest(params?: { authorization?: string; remoteAddress?: string; + url?: string; }): IncomingMessage { return { method: "POST", - url: "/hooks/wake", + url: params?.url ?? "/hooks/wake", headers: { host: "127.0.0.1:18789", authorization: params?.authorization ?? "Bearer hook-secret", @@ -71,10 +72,11 @@ function createResponse(): { function createHandler(params?: { dispatchWakeHook?: HooksHandlerDeps["dispatchWakeHook"]; dispatchAgentHook?: HooksHandlerDeps["dispatchAgentHook"]; + bindHost?: string; }) { return createHooksRequestHandler({ getHooksConfig: () => createHooksConfig(), - bindHost: "127.0.0.1", + bindHost: params?.bindHost ?? "127.0.0.1", port: 18789, logHooks: { warn: vi.fn(), @@ -139,4 +141,18 @@ describe("createHooksRequestHandler timeout status mapping", () => { expect(mappedRes.statusCode).toBe(429); expect(setHeader).toHaveBeenCalledWith("Retry-After", expect.any(String)); }); + + test.each(["0.0.0.0", "::"])( + "does not throw when bindHost=%s while parsing non-hook request URL", + async (bindHost) => { + const handler = createHandler({ bindHost }); + const req = createRequest({ url: "/" }); + const { res, end } = createResponse(); + + const handled = await handler(req, res); + + expect(handled).toBe(false); + expect(end).not.toHaveBeenCalled(); + }, + ); }); diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index 72a81a769ad..41d04d5d3ac 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -208,7 +208,7 @@ export function createHooksRequestHandler( logHooks: SubsystemLogger; } & HookDispatchers, ): HooksRequestHandler { - const { getHooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts; + const { getHooksConfig, logHooks, dispatchAgentHook, dispatchWakeHook } = opts; const hookAuthLimiter = createAuthRateLimiter({ maxAttempts: HOOK_AUTH_FAILURE_LIMIT, windowMs: HOOK_AUTH_FAILURE_WINDOW_MS, @@ -227,7 +227,9 @@ export function createHooksRequestHandler( if (!hooksConfig) { return false; } - const url = new URL(req.url ?? "/", `http://${bindHost}:${port}`); + // Only pathname/search are used here; keep the base host fixed so bind-host + // representation (e.g. IPv6 wildcards) cannot break request parsing. + const url = new URL(req.url ?? "/", "http://localhost"); const basePath = hooksConfig.basePath; if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) { return false;