diff --git a/CHANGELOG.md b/CHANGELOG.md index cd26aeed6..cf9b15a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -603,6 +603,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Models/provider config precedence: prefer exact `models.providers.` matches before normalized provider aliases in embedded model resolution, preventing alias/canonical key collisions from applying the wrong provider `api`, `baseUrl`, or headers. (#35934) thanks @RealKai42. +- Hooks/auth throttling: reject non-`POST` `/hooks/*` requests before auth-failure accounting so unsupported methods can no longer burn the hook auth lockout budget and block legitimate webhook delivery. Thanks @P1ck3d for reporting. - Network/fetch guard redirect auth stripping: switch cross-origin redirect handling in `fetchWithSsrFGuard` from a narrow sensitive-header denylist to a safe-header allowlist so custom auth headers like `X-Api-Key` and `Private-Token` no longer leak on origin changes. Thanks @Rickidevs for reporting. - Logging/Subsystem console timestamps: route subsystem console timestamp rendering through `formatConsoleTimestamp(...)` so `pretty` and timestamp-prefix output use local timezone formatting consistently instead of inline UTC `toISOString()` paths. (#25970) Thanks @openperf. - Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, prevent inbound preview text from leaking into prompt system events, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #31209, #29610, #30432, #30331, and #29501. Thanks @stakeswky, @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff. diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index 612ce90db..e5d2a5fa0 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -383,6 +383,14 @@ export function createHooksRequestHandler( return true; } + if (req.method !== "POST") { + res.statusCode = 405; + res.setHeader("Allow", "POST"); + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end("Method Not Allowed"); + return true; + } + const token = extractHookToken(req); const clientKey = resolveHookClientKey(req); if (!safeEqualSecret(token, hooksConfig.token)) { @@ -404,14 +412,6 @@ export function createHooksRequestHandler( } hookAuthLimiter.reset(clientKey, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH); - if (req.method !== "POST") { - res.statusCode = 405; - res.setHeader("Allow", "POST"); - res.setHeader("Content-Type", "text/plain; charset=utf-8"); - res.end("Method Not Allowed"); - return true; - } - const subPath = url.pathname.slice(basePath.length).replace(/^\/+/, ""); if (!subPath) { res.statusCode = 404; diff --git a/src/gateway/server.hooks.test.ts b/src/gateway/server.hooks.test.ts index 0c125600f..6711671e4 100644 --- a/src/gateway/server.hooks.test.ts +++ b/src/gateway/server.hooks.test.ts @@ -383,4 +383,24 @@ describe("gateway server hooks", () => { expect(failAfterSuccess.status).toBe(401); }); }); + + test("rejects non-POST hook requests without consuming auth failure budget", async () => { + testState.hooksConfig = { enabled: true, token: HOOK_TOKEN }; + await withGatewayServer(async ({ port }) => { + let lastGet: Response | null = null; + for (let i = 0; i < 21; i++) { + lastGet = await fetch(`http://127.0.0.1:${port}/hooks/wake`, { + method: "GET", + headers: { Authorization: "Bearer wrong" }, + }); + } + expect(lastGet?.status).toBe(405); + expect(lastGet?.headers.get("allow")).toBe("POST"); + + const allowed = await postHook(port, "/hooks/wake", { text: "still works" }); + expect(allowed.status).toBe(200); + await waitForSystemEvent(); + drainSystemEvents(resolveMainKey()); + }); + }); });