Root cause
Authentication & access control
fix: allow token auth to bypass device identity requirement
How AI contributed: New attack surfacePublished Mar 3, 2026 in
openclaw/openclawAll 35 findings in this repository
Description In affected releases, when an operator explicitly enabled gateway.controlUi.allowInsecureAuth: true and exposed the gateway over plaintext HTTP, Control UI authentication could permit privileged operator access without the intended device identity + pairing guarantees. This required an insecure deployment choice and credential exposure risk (for example, plaintext transit or prior token leak). It was fixed on main in commit 40a292619e1f2be3a3b1db663d7494c9c2dc0abf (PR #20684). Affected Packages / Versions · Package: openclaw (npm) · Affected published versions: <= 2026.2.19-2 · Planned patched version: 2026.2.21 Impact In these explicitly insecure deployments, an attacker with leaked/intercepted credentials could obtain high-privilege Control UI access. Fix Commit(s) · 40a292619e1f2be3a3b1db663d7494c9c2dc0abf (merged 2026-02-20) OpenClaw thanks @Vasco0x4 for reporting.
message-handler.ts skipped the required device identity check whenever the handshake carried an auth token, so a token-only Control UI connection could be accepted even when the gateway was served over plaintext HTTP with gateway.controlUi.allowInsecureAuth enabled. The fix ties the control-ui exemption to the explicit device-auth disable flag, so allowInsecureAuth alone can no longer satisfy the handshake without a paired device.
Root cause
fix: allow token auth to bypass device identity requirement
Fix
fix: Control UI Insecure Auth Bypass Allows Token-Only Auth Over HTTP (#20684)
Fix by Coy Geek · no AI marker found
Case facts
@@ -254,7 +254,9 @@ export function attachGatewayWsMessageHandler(params: { const device = connectParams.device; let devicePublicKey: string | null = null;- if (!device) {+ // Allow token-authenticated connections (e.g., control-ui) to skip device identity+ const hasTokenAuth = !!connectParams.auth?.token;+ if (!device && !hasTokenAuth) { setHandshakeState("failed"); setCloseCause("device-required", { client: connectParams.client.id,@@ -427,7 +429,7 @@ export function attachGatewayWsMessageHandler(params: { }); let authOk = authResult.ok; let authMethod = authResult.method ?? "none";- if (!authOk && connectParams.auth?.token) {+ if (!authOk && connectParams.auth?.token && device) { const tokenCheck = await verifyDeviceToken({ deviceId: device.id, token: connectParams.auth.token,@@ -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.", }); }@@ -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;@@ -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 {AI-assisted change b94c5c5ced25b9bce426afccf67da47d98569efd9eb060e7500c7277aaa1278b · Fix a55cf7f65c9ee927890cf7a9a50519f225e903746f277a82a853816ad7c9fddc