From fc617945ff20bd5d6a7250a6b0f7655d402197df Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:31:14 -0600 Subject: [PATCH] fix(gateway): handle malformed tool payload validation safely --- CHANGELOG.md | 1 + src/gateway/tools-invoke-http.test.ts | 19 ++++++++++++++++++- src/gateway/tools-invoke-http.ts | 8 ++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d0dfe4003..8bfe2ada3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/tools invoke malformed-tool hardening: classify tool schema validation failures (including Zod-style errors) as controlled `400 tool_error` responses instead of internal failures, preventing malformed tool payloads from escalating into unstable runtime behavior. (#38384) - Onboarding/headless Linux daemon probe hardening: treat `systemctl --user is-enabled` probe failures as non-fatal during daemon install flow so onboarding no longer crashes on SSH/headless VPS environments before showing install guidance. (#37297) Thanks @acarbajal-web. - Memory/QMD mcporter Windows spawn hardening: when `mcporter.cmd` launch fails with `spawn EINVAL`, retry via bare `mcporter` shell resolution so QMD recall can continue instead of falling back to builtin memory search. (#27402) Thanks @i0ivi0i. - Tools/web_search Brave language-code validation: align `search_lang` handling with Brave-supported codes (including `zh-hans`, `zh-hant`, `en-gb`, and `pt-br`), map common alias inputs (`zh`, `ja`) to valid Brave values, and reject unsupported codes before upstream requests to prevent 422 failures. (#37260) Thanks @heyanming. diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index 66a68bf5d9f..123cefebc17 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -117,6 +117,12 @@ vi.mock("../agents/openclaw-tools.js", () => { if (mode === "auth") { throw toolAuthorizationError("mode forbidden"); } + if (mode === "zod") { + const err = new Error("invalid tool payload") as Error & { issues?: unknown[] }; + err.name = "ZodError"; + err.issues = [{ path: ["amount"], message: "Required", code: "invalid_type" }]; + throw err; + } if (mode === "crash") { throw new Error("boom"); } @@ -540,7 +546,7 @@ describe("POST /tools/invoke", () => { expect(resMain.status).toBe(200); }); - it("maps tool input/auth errors to 400/403 and unexpected execution errors to 500", async () => { + it("maps tool input/auth/schema errors to 400/403 and unexpected execution errors to 500", async () => { cfg = { ...cfg, agents: { @@ -570,6 +576,17 @@ describe("POST /tools/invoke", () => { expect(authBody.error?.type).toBe("tool_error"); expect(authBody.error?.message).toBe("mode forbidden"); + const zodRes = await invokeToolAuthed({ + tool: "tools_invoke_test", + args: { mode: "zod" }, + sessionKey: "main", + }); + expect(zodRes.status).toBe(400); + const zodBody = await zodRes.json(); + expect(zodBody.ok).toBe(false); + expect(zodBody.error?.type).toBe("tool_error"); + expect(zodBody.error?.message).toBe("invalid tool payload"); + const crashRes = await invokeToolAuthed({ tool: "tools_invoke_test", args: { mode: "crash" }, diff --git a/src/gateway/tools-invoke-http.ts b/src/gateway/tools-invoke-http.ts index 88cea7b3845..afd860270e9 100644 --- a/src/gateway/tools-invoke-http.ts +++ b/src/gateway/tools-invoke-http.ts @@ -117,6 +117,14 @@ function resolveToolInputErrorStatus(err: unknown): number | null { const status = (err as { status?: unknown }).status; return typeof status === "number" ? status : 400; } + // Some tool implementations throw raw ZodError objects on malformed model/tool payloads. + // Treat these as user-input validation failures (400), not internal server failures (500). + if (typeof err === "object" && err !== null) { + const record = err as { name?: unknown; issues?: unknown }; + if (record.name === "ZodError" && Array.isArray(record.issues)) { + return 400; + } + } if (typeof err !== "object" || err === null || !("name" in err)) { return null; }