test(gateway): add hooks bind-host hardening coverage

This commit is contained in:
Peter Steinberger
2026-02-26 01:54:10 +01:00
parent 3cd3d489f4
commit 8c701ba1ff

View File

@@ -1,7 +1,9 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { describe, expect, test, vi } from "vitest";
import type { createSubsystemLogger } from "../logging/subsystem.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import { createGatewayHttpServer } from "./server-http.js";
import type { HooksConfigResolved } from "./hooks.js";
import { createGatewayHttpServer, createHooksRequestHandler } from "./server-http.js";
import { withTempConfig } from "./test-temp-config.js";
function createRequest(params: {
@@ -65,6 +67,25 @@ async function dispatchRequest(
await new Promise((resolve) => setImmediate(resolve));
}
function createHooksConfig(): HooksConfigResolved {
return {
basePath: "/hooks",
token: "hook-secret",
maxBodyBytes: 1024,
mappings: [],
agentPolicy: {
defaultAgentId: "main",
knownAgentIds: new Set(["main"]),
allowedAgentIds: undefined,
},
sessionPolicy: {
allowRequestSessionKey: false,
defaultSessionKey: undefined,
allowedSessionKeyPrefixes: undefined,
},
};
}
describe("gateway plugin HTTP auth boundary", () => {
test("applies default security headers and optional strict transport security", async () => {
const resolvedAuth: ResolvedGatewayAuth = {
@@ -220,4 +241,101 @@ describe("gateway plugin HTTP auth boundary", () => {
},
});
});
test.each(["0.0.0.0", "::"])(
"returns 404 (not 500) for non-hook routes with hooks enabled and bindHost=%s",
async (bindHost) => {
const resolvedAuth: ResolvedGatewayAuth = {
mode: "none",
token: undefined,
password: undefined,
allowTailscale: false,
};
await withTempConfig({
cfg: { gateway: { trustedProxies: [] } },
prefix: "openclaw-plugin-http-hooks-bindhost-",
run: async () => {
const handleHooksRequest = createHooksRequestHandler({
getHooksConfig: () => createHooksConfig(),
bindHost,
port: 18789,
logHooks: {
warn: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
} as unknown as ReturnType<typeof createSubsystemLogger>,
dispatchWakeHook: () => {},
dispatchAgentHook: () => "run-1",
});
const server = createGatewayHttpServer({
canvasHost: null,
clients: new Set(),
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
handleHooksRequest,
resolvedAuth,
});
const response = createResponse();
await dispatchRequest(server, createRequest({ path: "/" }), response.res);
expect(response.res.statusCode).toBe(404);
expect(response.getBody()).toBe("Not Found");
},
});
},
);
test("rejects query-token hooks requests with bindHost=::", async () => {
const resolvedAuth: ResolvedGatewayAuth = {
mode: "none",
token: undefined,
password: undefined,
allowTailscale: false,
};
await withTempConfig({
cfg: { gateway: { trustedProxies: [] } },
prefix: "openclaw-plugin-http-hooks-query-token-",
run: async () => {
const handleHooksRequest = createHooksRequestHandler({
getHooksConfig: () => createHooksConfig(),
bindHost: "::",
port: 18789,
logHooks: {
warn: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
} as unknown as ReturnType<typeof createSubsystemLogger>,
dispatchWakeHook: () => {},
dispatchAgentHook: () => "run-1",
});
const server = createGatewayHttpServer({
canvasHost: null,
clients: new Set(),
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
handleHooksRequest,
resolvedAuth,
});
const response = createResponse();
await dispatchRequest(
server,
createRequest({ path: "/hooks/wake?token=bad" }),
response.res,
);
expect(response.res.statusCode).toBe(400);
expect(response.getBody()).toContain("Hook token must be provided");
},
});
});
});