From ad9be4b4d65698785ad7ea9ad650f54d16c89c4a Mon Sep 17 00:00:00 2001 From: mbelinky Date: Fri, 20 Feb 2026 18:32:33 +0100 Subject: [PATCH] Gateway/Security: enforce secure control-ui auth path openclaw#20684 thanks @coygeek --- CHANGELOG.md | 1 + src/gateway/server.auth.e2e.test.ts | 28 ++++++++++++++++--- .../server/ws-connection/message-handler.ts | 8 ++++-- src/security/audit.ts | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df26caf8b62..046c7b4245a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/Security: require secure context and paired-device checks for Control UI auth even when `gateway.controlUi.allowInsecureAuth` is set, and align audit messaging with the hardened behavior. (#20684) thanks @coygeek. - macOS/Build: default release packaging to `BUNDLE_ID=ai.openclaw.mac` in `scripts/package-mac-dist.sh`, so Sparkle feed URL is retained and auto-update no longer fails with an empty appcast feed. (#19750) thanks @loganprit. - Signal/Outbound: preserve case for Base64 group IDs during outbound target normalization so cross-context routing and policy checks no longer break when group IDs include uppercase characters. (#5578) Thanks @heyhudson. diff --git a/src/gateway/server.auth.e2e.test.ts b/src/gateway/server.auth.e2e.test.ts index b529a9ff2f3..01c5e84387d 100644 --- a/src/gateway/server.auth.e2e.test.ts +++ b/src/gateway/server.auth.e2e.test.ts @@ -687,7 +687,7 @@ describe("gateway server auth/connect", () => { }); }); - test("allows control ui without device identity when insecure auth is enabled", async () => { + test("rejects control ui without device identity even when insecure auth is enabled", async () => { testState.gatewayControlUi = { allowInsecureAuth: true }; const { server, ws, prevToken } = await startServerWithClient("secret", { wsHeaders: { origin: "http://127.0.0.1" }, @@ -702,13 +702,32 @@ describe("gateway server auth/connect", () => { mode: GATEWAY_CLIENT_MODES.WEBCHAT, }, }); - expect(res.ok).toBe(true); + expect(res.ok).toBe(false); + expect(res.error?.message ?? "").toContain("secure context"); ws.close(); await server.close(); restoreGatewayToken(prevToken); }); - test("allows control ui with device identity when insecure auth is enabled", async () => { + test("rejects control ui password-only auth when insecure auth is enabled", async () => { + testState.gatewayControlUi = { allowInsecureAuth: true }; + testState.gatewayAuth = { mode: "password", password: "secret" }; + await withGatewayServer(async ({ port }) => { + const ws = await openWs(port, { origin: originForPort(port) }); + const res = await connectReq(ws, { + password: "secret", + device: null, + client: { + ...CONTROL_UI_CLIENT, + }, + }); + expect(res.ok).toBe(false); + expect(res.error?.message ?? "").toContain("secure context"); + ws.close(); + }); + }); + + test("does not bypass pairing for control ui device identity when insecure auth is enabled", async () => { testState.gatewayControlUi = { allowInsecureAuth: true }; testState.gatewayAuth = { mode: "token", token: "secret" }; const { writeConfigFile } = await import("../config/config.js"); @@ -753,7 +772,8 @@ describe("gateway server auth/connect", () => { ...CONTROL_UI_CLIENT, }, }); - expect(res.ok).toBe(true); + expect(res.ok).toBe(false); + expect(res.error?.message ?? "").toContain("pairing required"); ws.close(); }); } finally { diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index e265039ff6a..2f0550854e6 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -341,7 +341,9 @@ export function attachGatewayWsMessageHandler(params: { isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true; const disableControlUiDeviceAuth = isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true; - const allowControlUiBypass = allowInsecureControlUi || disableControlUiDeviceAuth; + // `allowInsecureAuth` is retained for compatibility, but must not bypass + // secure-context/device-auth requirements. + const allowControlUiBypass = disableControlUiDeviceAuth; const device = disableControlUiDeviceAuth ? null : deviceRaw; const hasDeviceTokenCandidate = Boolean(connectParams.auth?.token && device); @@ -428,7 +430,9 @@ export function attachGatewayWsMessageHandler(params: { if (isControlUi && !allowControlUiBypass) { const errorMessage = "control ui requires HTTPS or localhost (secure context)"; - markHandshakeFailure("control-ui-insecure-auth"); + markHandshakeFailure("control-ui-insecure-auth", { + insecureAuthConfigured: allowInsecureControlUi, + }); sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, errorMessage); close(1008, errorMessage); return; diff --git a/src/security/audit.ts b/src/security/audit.ts index 2fc4d000919..7d79c18a386 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -351,7 +351,7 @@ function collectGatewayConfigFindings( severity: "critical", title: "Control UI allows insecure HTTP auth", detail: - "gateway.controlUi.allowInsecureAuth=true allows token-only auth over HTTP and skips device identity.", + "gateway.controlUi.allowInsecureAuth=true is a legacy insecure-auth toggle; Control UI still enforces secure context and device identity unless dangerouslyDisableDeviceAuth is enabled.", remediation: "Disable it or switch to HTTPS (Tailscale Serve) or localhost.", }); }