From d51a4695f0cefbed1df0795fb82c27223282caab Mon Sep 17 00:00:00 2001 From: Brian Mendonca Date: Mon, 23 Feb 2026 21:18:10 -0700 Subject: [PATCH] Deny cron tool on /tools/invoke by default (cherry picked from commit 816a6b3a4df5bf8436f08e3fc8fa82411e3543ac) --- .../tools-invoke-http.cron-regression.test.ts | 123 ++++++++++++++++++ src/security/dangerous-tools.ts | 2 + 2 files changed, 125 insertions(+) create mode 100644 src/gateway/tools-invoke-http.cron-regression.test.ts diff --git a/src/gateway/tools-invoke-http.cron-regression.test.ts b/src/gateway/tools-invoke-http.cron-regression.test.ts new file mode 100644 index 000000000..a3df26338 --- /dev/null +++ b/src/gateway/tools-invoke-http.cron-regression.test.ts @@ -0,0 +1,123 @@ +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TEST_GATEWAY_TOKEN = "test-gateway-token-1234567890"; + +let cfg: Record = {}; + +vi.mock("../config/config.js", () => ({ + loadConfig: () => cfg, +})); + +vi.mock("../config/sessions.js", () => ({ + resolveMainSessionKey: () => "agent:main:main", +})); + +vi.mock("./auth.js", () => ({ + authorizeHttpGatewayConnect: async () => ({ ok: true }), +})); + +vi.mock("../logger.js", () => ({ + logWarn: () => {}, +})); + +vi.mock("../plugins/config-state.js", () => ({ + isTestDefaultMemorySlotDisabled: () => false, +})); + +vi.mock("../plugins/tools.js", () => ({ + getPluginToolMeta: () => undefined, +})); + +vi.mock("../agents/openclaw-tools.js", () => { + const tools = [ + { + name: "cron", + parameters: { type: "object", properties: { action: { type: "string" } } }, + execute: async () => ({ ok: true, via: "cron" }), + }, + { + name: "gateway", + parameters: { type: "object", properties: { action: { type: "string" } } }, + execute: async () => ({ ok: true, via: "gateway" }), + }, + ]; + return { + createOpenClawTools: () => tools, + }; +}); + +const { handleToolsInvokeHttpRequest } = await import("./tools-invoke-http.js"); + +let port = 0; +let server: ReturnType | undefined; + +beforeAll(async () => { + server = createServer((req, res) => { + void handleToolsInvokeHttpRequest(req, res, { + auth: { mode: "token", token: TEST_GATEWAY_TOKEN, allowTailscale: false }, + }).then((handled) => { + if (handled) { + return; + } + res.statusCode = 404; + res.end("not found"); + }); + }); + await new Promise((resolve, reject) => { + server?.once("error", reject); + server?.listen(0, "127.0.0.1", () => { + const address = server?.address() as AddressInfo | null; + port = address?.port ?? 0; + resolve(); + }); + }); +}); + +afterAll(async () => { + if (!server) { + return; + } + await new Promise((resolve) => server?.close(() => resolve())); + server = undefined; +}); + +beforeEach(() => { + cfg = {}; +}); + +async function invoke(tool: string) { + return await fetch(`http://127.0.0.1:${port}/tools/invoke`, { + method: "POST", + headers: { + "content-type": "application/json", + authorization: `Bearer ${TEST_GATEWAY_TOKEN}`, + }, + body: JSON.stringify({ tool, action: "status", args: {}, sessionKey: "main" }), + }); +} + +describe("tools invoke HTTP denylist", () => { + it("blocks cron and gateway by default", async () => { + const gatewayRes = await invoke("gateway"); + const cronRes = await invoke("cron"); + + expect(gatewayRes.status).toBe(404); + expect(cronRes.status).toBe(404); + }); + + it("allows cron only when explicitly enabled in gateway.tools.allow", async () => { + cfg = { + gateway: { + tools: { + allow: ["cron"], + }, + }, + }; + + const cronRes = await invoke("cron"); + + expect(cronRes.status).toBe(200); + }); +}); diff --git a/src/security/dangerous-tools.ts b/src/security/dangerous-tools.ts index be585913b..6d1274723 100644 --- a/src/security/dangerous-tools.ts +++ b/src/security/dangerous-tools.ts @@ -11,6 +11,8 @@ export const DEFAULT_GATEWAY_HTTP_TOOL_DENY = [ "sessions_spawn", // Cross-session injection — message injection across sessions "sessions_send", + // Persistent automation control plane — can create/update/remove scheduled runs + "cron", // Gateway control plane — prevents gateway reconfiguration via HTTP "gateway", // Interactive setup — requires terminal QR scan, hangs on HTTP