mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(feishu): normalize webhook rate-limit client keys [AI] (#80975)
* fix: normalize Feishu webhook rate limit keys * addressing claude review * docs: add changelog entry for PR merge
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix(feishu): normalize webhook rate-limit client keys [AI]. (#80975) Thanks @pgondhi987.
|
||||
- fix(auth): prevent bootstrap pairing scope changes [AI]. (#80976) Thanks @pgondhi987.
|
||||
- Validate Control UI loopback retry endpoints [AI]. (#80900) Thanks @pgondhi987.
|
||||
- Harden exported markdown link rendering [AI]. (#80902) Thanks @pgondhi987.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export type { RuntimeEnv } from "../runtime-api.js";
|
||||
export { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
||||
export { applyBasicWebhookRequestGuards } from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export {
|
||||
applyBasicWebhookRequestGuards,
|
||||
resolveRequestClientIp,
|
||||
} from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export {
|
||||
installRequestBodyLimitGuard,
|
||||
readWebhookBodyOrReject,
|
||||
|
||||
@@ -5,10 +5,11 @@ import { waitForAbortableDelay } from "./async.js";
|
||||
import { createFeishuWSClient } from "./client.js";
|
||||
import {
|
||||
applyBasicWebhookRequestGuards,
|
||||
type RuntimeEnv,
|
||||
installRequestBodyLimitGuard,
|
||||
readWebhookBodyOrReject,
|
||||
resolveRequestClientIp,
|
||||
safeEqualSecret,
|
||||
type RuntimeEnv,
|
||||
} from "./monitor-transport-runtime-api.js";
|
||||
import {
|
||||
botNames,
|
||||
@@ -90,6 +91,28 @@ function respondText(res: http.ServerResponse, statusCode: number, body: string)
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function normalizeFeishuWebhookRateLimitClient(clientIp: string | undefined): string {
|
||||
if (!clientIp) {
|
||||
return "unknown";
|
||||
}
|
||||
if (clientIp === "::1" || clientIp.startsWith("127.")) {
|
||||
return "loopback";
|
||||
}
|
||||
return clientIp;
|
||||
}
|
||||
|
||||
function buildFeishuWebhookRateLimitKey(params: {
|
||||
accountId: string;
|
||||
path: string;
|
||||
clientIp?: string;
|
||||
}): string {
|
||||
return `${params.accountId}:${params.path}:${normalizeFeishuWebhookRateLimitClient(
|
||||
params.clientIp,
|
||||
)}`;
|
||||
}
|
||||
|
||||
export { buildFeishuWebhookRateLimitKey as buildFeishuWebhookRateLimitKeyForTest };
|
||||
|
||||
function getFeishuWsReconnectDelayMs(attempt: number): number {
|
||||
return Math.min(
|
||||
FEISHU_WS_RECONNECT_INITIAL_DELAY_MS * 2 ** Math.max(0, attempt - 1),
|
||||
@@ -300,7 +323,11 @@ export async function monitorWebhook({
|
||||
recordWebhookStatus(runtime, accountId, path, res.statusCode);
|
||||
});
|
||||
|
||||
const rateLimitKey = `${accountId}:${path}:${req.socket.remoteAddress ?? "unknown"}`;
|
||||
const rateLimitKey = buildFeishuWebhookRateLimitKey({
|
||||
accountId,
|
||||
path,
|
||||
clientIp: resolveRequestClientIp(req),
|
||||
});
|
||||
if (
|
||||
!applyBasicWebhookRequestGuards({
|
||||
req,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { createConnection } from "node:net";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ vi.mock("./monitor.state.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import type { RuntimeEnv } from "../runtime-api.js";
|
||||
import { resolveRequestClientIp } from "./monitor-transport-runtime-api.js";
|
||||
import {
|
||||
clearFeishuWebhookRateLimitStateForTest,
|
||||
getFeishuWebhookRateLimitStateSizeForTest,
|
||||
@@ -44,7 +46,7 @@ import {
|
||||
monitorFeishuProvider,
|
||||
stopFeishuMonitor,
|
||||
} from "./monitor.js";
|
||||
import { monitorWebhook } from "./monitor.transport.js";
|
||||
import { buildFeishuWebhookRateLimitKeyForTest, monitorWebhook } from "./monitor.transport.js";
|
||||
import type { ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
async function waitForSlowBodyTimeoutResponse(
|
||||
@@ -150,6 +152,13 @@ async function waitForOversizedBodyResponse(url: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTestClientIp(remoteAddress: string | undefined): string | undefined {
|
||||
return resolveRequestClientIp({
|
||||
headers: {},
|
||||
socket: { remoteAddress },
|
||||
} as IncomingMessage);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearFeishuWebhookRateLimitStateForTest();
|
||||
stopFeishuMonitor();
|
||||
@@ -314,6 +323,51 @@ describe("Feishu webhook security hardening", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses one webhook rate-limit key for loopback address-family variants", () => {
|
||||
const base = {
|
||||
accountId: "rate-limit-key",
|
||||
path: "/hook-rate-limit-key",
|
||||
};
|
||||
|
||||
expect([
|
||||
buildFeishuWebhookRateLimitKeyForTest({
|
||||
...base,
|
||||
clientIp: resolveTestClientIp("127.0.0.1"),
|
||||
}),
|
||||
buildFeishuWebhookRateLimitKeyForTest({
|
||||
...base,
|
||||
clientIp: resolveTestClientIp("127.0.0.42"),
|
||||
}),
|
||||
buildFeishuWebhookRateLimitKeyForTest({
|
||||
...base,
|
||||
clientIp: resolveTestClientIp("::ffff:127.0.0.1"),
|
||||
}),
|
||||
buildFeishuWebhookRateLimitKeyForTest({
|
||||
...base,
|
||||
clientIp: resolveTestClientIp("::1"),
|
||||
}),
|
||||
]).toEqual([
|
||||
"rate-limit-key:/hook-rate-limit-key:loopback",
|
||||
"rate-limit-key:/hook-rate-limit-key:loopback",
|
||||
"rate-limit-key:/hook-rate-limit-key:loopback",
|
||||
"rate-limit-key:/hook-rate-limit-key:loopback",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps non-loopback and unknown webhook rate-limit key suffixes distinct", () => {
|
||||
const base = {
|
||||
accountId: "rate-limit-key",
|
||||
path: "/hook-rate-limit-key",
|
||||
};
|
||||
|
||||
expect(buildFeishuWebhookRateLimitKeyForTest({ ...base, clientIp: "10.0.0.1" })).toBe(
|
||||
"rate-limit-key:/hook-rate-limit-key:10.0.0.1",
|
||||
);
|
||||
expect(buildFeishuWebhookRateLimitKeyForTest(base)).toBe(
|
||||
"rate-limit-key:/hook-rate-limit-key:unknown",
|
||||
);
|
||||
});
|
||||
|
||||
it("caps tracked webhook rate-limit keys to prevent unbounded growth", () => {
|
||||
const now = 1_000_000;
|
||||
for (let i = 0; i < 4_500; i += 1) {
|
||||
|
||||
Reference in New Issue
Block a user