fix(gateway): harden hooks URL parsing (#26864)

This commit is contained in:
Peter Steinberger
2026-02-26 01:47:07 +01:00
parent c0026274d9
commit 70e31c6f68
2 changed files with 22 additions and 4 deletions

View File

@@ -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();
},
);
});

View File

@@ -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;