diff --git a/CHANGELOG.md b/CHANGELOG.md index 1144b4fcd6d..6e031c51f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,7 @@ Docs: https://docs.openclaw.ai - Mattermost/DM send: retry transient direct-channel creation failures for DM deliveries, with configurable backoff and per-request timeout. (#42398) Thanks @JonathanJing. - Telegram/network: unify API and media fetches under the same sticky IPv4 and pinned-IP fallback chain, and re-validate pinned override addresses against SSRF policy. (#49148) Thanks @obviyus. - Agents/prompt composition: append bootstrap truncation warnings to the current-turn prompt and add regression coverage for stable system-prompt cache invariants. (#49237) Thanks @scoootscooob. +- Gateway/auth: add regression coverage that keeps device-less trusted-proxy Control UI sessions off privileged pairing approval RPCs. Thanks @vincentkoc. ### Breaking diff --git a/src/gateway/server.auth.control-ui.suite.ts b/src/gateway/server.auth.control-ui.suite.ts index 9452c26eb33..294fb0dcad8 100644 --- a/src/gateway/server.auth.control-ui.suite.ts +++ b/src/gateway/server.auth.control-ui.suite.ts @@ -112,6 +112,12 @@ export function registerControlUiAndPairingSuite(): void { expect(talk.error?.message).toBe("missing scope: operator.read"); }; + const expectDevicePairApproveDenied = async (ws: WebSocket, requestId: string) => { + const approve = await rpcReq(ws, "device.pair.approve", { requestId }); + expect(approve.ok).toBe(false); + expect(approve.error?.message).toBe("missing scope: operator.admin"); + }; + const connectControlUiWithoutDeviceAndExpectOk = async (params: { ws: WebSocket; token?: string; @@ -244,6 +250,17 @@ export function registerControlUiAndPairingSuite(): void { test("clears self-declared scopes for trusted-proxy control ui without device identity", async () => { await configureTrustedProxyControlUiAuth(); + const { publicKeyRawBase64UrlFromPem } = await import("../infra/device-identity.js"); + const { requestDevicePairing } = await import("../infra/device-pairing.js"); + const { identity } = await createOperatorIdentityFixture("openclaw-control-ui-trusted-proxy-"); + const pendingRequest = await requestDevicePairing({ + deviceId: identity.deviceId, + publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), + role: "operator", + scopes: ["operator.admin"], + clientId: CONTROL_UI_CLIENT.id, + clientMode: CONTROL_UI_CLIENT.mode, + }); await withGatewayServer(async ({ port }) => { const ws = await openWs(port, TRUSTED_PROXY_CONTROL_UI_HEADERS); try { @@ -259,6 +276,7 @@ export function registerControlUiAndPairingSuite(): void { await expectStatusMissingScopeButHealthOk(ws); await expectAdminRpcDenied(ws); await expectTalkSecretsDenied(ws); + await expectDevicePairApproveDenied(ws, pendingRequest.request.requestId); } finally { ws.close(); }