fix(security): harden gateway command/audit guardrails

This commit is contained in:
Peter Steinberger
2026-02-22 08:44:12 +01:00
parent 121d027229
commit 265da4dd2a
10 changed files with 176 additions and 28 deletions

View File

@@ -21,6 +21,13 @@ function normalizePart(value: unknown, fallback: string): string {
export function resolveControlPlaneRateLimitKey(client: GatewayClient | null): string {
const deviceId = normalizePart(client?.connect?.device?.id, "unknown-device");
const clientIp = normalizePart(client?.clientIp, "unknown-ip");
if (deviceId === "unknown-device" && clientIp === "unknown-ip") {
// Last-resort fallback: avoid cross-client contention when upstream identity is missing.
const connId = normalizePart(client?.connId, "");
if (connId) {
return `${deviceId}|${clientIp}|conn=${connId}`;
}
}
return `${deviceId}|${clientIp}`;
}

View File

@@ -1,5 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { __testing as controlPlaneRateLimitTesting } from "./control-plane-rate-limit.js";
import {
__testing as controlPlaneRateLimitTesting,
resolveControlPlaneRateLimitKey,
} from "./control-plane-rate-limit.js";
import { handleGatewayRequest } from "./server-methods.js";
import type { GatewayRequestHandler } from "./server-methods/types.js";
@@ -121,4 +124,43 @@ describe("gateway control-plane write rate limit", () => {
expect(allowed).toHaveBeenCalledWith(true, undefined, undefined);
expect(handlerCalls).toHaveBeenCalledTimes(4);
});
it("uses connId fallback when both device and client IP are unknown", () => {
const key = resolveControlPlaneRateLimitKey({
connect: {
role: "operator",
scopes: ["operator.admin"],
client: {
id: "openclaw-control-ui",
version: "1.0.0",
platform: "darwin",
mode: "ui",
},
minProtocol: 1,
maxProtocol: 1,
},
connId: "conn-fallback",
});
expect(key).toBe("unknown-device|unknown-ip|conn=conn-fallback");
});
it("keeps device/IP-based key when identity is present", () => {
const key = resolveControlPlaneRateLimitKey({
connect: {
role: "operator",
scopes: ["operator.admin"],
client: {
id: "openclaw-control-ui",
version: "1.0.0",
platform: "darwin",
mode: "ui",
},
minProtocol: 1,
maxProtocol: 1,
},
connId: "conn-fallback",
clientIp: "10.0.0.10",
});
expect(key).toBe("unknown-device|10.0.0.10");
});
});