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?: { function createRequest(params?: {
authorization?: string; authorization?: string;
remoteAddress?: string; remoteAddress?: string;
url?: string;
}): IncomingMessage { }): IncomingMessage {
return { return {
method: "POST", method: "POST",
url: "/hooks/wake", url: params?.url ?? "/hooks/wake",
headers: { headers: {
host: "127.0.0.1:18789", host: "127.0.0.1:18789",
authorization: params?.authorization ?? "Bearer hook-secret", authorization: params?.authorization ?? "Bearer hook-secret",
@@ -71,10 +72,11 @@ function createResponse(): {
function createHandler(params?: { function createHandler(params?: {
dispatchWakeHook?: HooksHandlerDeps["dispatchWakeHook"]; dispatchWakeHook?: HooksHandlerDeps["dispatchWakeHook"];
dispatchAgentHook?: HooksHandlerDeps["dispatchAgentHook"]; dispatchAgentHook?: HooksHandlerDeps["dispatchAgentHook"];
bindHost?: string;
}) { }) {
return createHooksRequestHandler({ return createHooksRequestHandler({
getHooksConfig: () => createHooksConfig(), getHooksConfig: () => createHooksConfig(),
bindHost: "127.0.0.1", bindHost: params?.bindHost ?? "127.0.0.1",
port: 18789, port: 18789,
logHooks: { logHooks: {
warn: vi.fn(), warn: vi.fn(),
@@ -139,4 +141,18 @@ describe("createHooksRequestHandler timeout status mapping", () => {
expect(mappedRes.statusCode).toBe(429); expect(mappedRes.statusCode).toBe(429);
expect(setHeader).toHaveBeenCalledWith("Retry-After", expect.any(String)); 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; logHooks: SubsystemLogger;
} & HookDispatchers, } & HookDispatchers,
): HooksRequestHandler { ): HooksRequestHandler {
const { getHooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts; const { getHooksConfig, logHooks, dispatchAgentHook, dispatchWakeHook } = opts;
const hookAuthLimiter = createAuthRateLimiter({ const hookAuthLimiter = createAuthRateLimiter({
maxAttempts: HOOK_AUTH_FAILURE_LIMIT, maxAttempts: HOOK_AUTH_FAILURE_LIMIT,
windowMs: HOOK_AUTH_FAILURE_WINDOW_MS, windowMs: HOOK_AUTH_FAILURE_WINDOW_MS,
@@ -227,7 +227,9 @@ export function createHooksRequestHandler(
if (!hooksConfig) { if (!hooksConfig) {
return false; 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; const basePath = hooksConfig.basePath;
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) { if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
return false; return false;