Root cause
Authentication & access control introduced
fix: allow token auth to bypass device identity requirement
How AI contributed: Direct introductionPublished Feb 17, 2026 in
openclaw/openclawAll 35 findings in this repository
Summary The gateway WebSocket connect handshake could allow skipping device identity checks when auth.token was present but not yet validated. Details In src/gateway/server/ws-connection/message-handler.ts, the device-identity requirement could be bypassed based on the *presence* of a non-empty connectParams.auth.token rather than a *validated* shared-secret authentication result. Impact In deployments where the gateway WebSocket is reachable and connections can be authorized via Tailscale without validating the shared secret, a client could connect without providing device identity/pairing. Depending on version and configuration, this could result in operator access. Deployment Guidance Per OpenClaw security guidance, the gateway should only be reachable from a trusted network and by trusted users (for example, restrict Tailnet users/ACLs when using Tailscale Serve). If the gateway WebSocket is only reachable by trusted users, there is typically no untrusted party with network access to exploit this issue. Affected Packages / Versions · Package: openclaw (npm) · Affected: <= 2026.2.1 · Fixed: >= 2026.2.2 Fix Device-identity skipping now requires *validated* shared-secret authentication (token/password). Tailscale-authenticated connections without validated shared secret require device identity. Fix Commit(s) · fe81b1d7125a014b8280da461f34efbf5f761575 Thanks @simecek for reporting.
When the advisory text quotes the exact token-presence predicate, that sibling of a shared SHA can CONFIRM even if other siblings stay NARROW.
Root cause
fix: allow token auth to bypass device identity requirement
Fix
fix(gateway): require shared auth before device bypass
Fix by Peter Steinberger · no AI marker found
Case facts
--- a/src/gateway/server/ws-connection/message-handler.ts+++ b/src/gateway/server/ws-connection/message-handler.ts@@ -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,--- a/src/gateway/server/ws-connection/message-handler.ts+++ b/src/gateway/server/ws-connection/message-handler.ts@@ -377,8 +377,63 @@ export function attachGatewayWsMessageHandler(params: { isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true; const allowControlUiBypass = allowInsecureControlUi || disableControlUiDeviceAuth; const device = disableControlUiDeviceAuth ? null : deviceRaw;++ const authResult = await authorizeGatewayConnect({+ auth: resolvedAuth,+ connectAuth: connectParams.auth,+ req: upgradeReq,+ trustedProxies,+ });+ let authOk = authResult.ok;+ let authMethod =+ authResult.method ?? (resolvedAuth.mode === "password" ? "password" : "token");+ const sharedAuthResult = hasSharedAuth+ ? await authorizeGatewayConnect({+ auth: { ...resolvedAuth, allowTailscale: false },+ connectAuth: connectParams.auth,+ req: upgradeReq,+ trustedProxies,+ })+ : null;+ const sharedAuthOk =+ sharedAuthResult?.ok === true &&+ (sharedAuthResult.method === "token" || sharedAuthResult.method === "password");+ const rejectUnauthorized = () => {+ setHandshakeState("failed");+ logWsControl.warn(+ `unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} reason=${authResult.reason ?? "unknown"}`,+ );+ const authProvided: AuthProvidedKind = connectParams.auth?.token+ ? "token"+ : connectParams.auth?.password+ ? "password"+ : "none";+ const authMessage = formatGatewayAuthFailureMessage({+ authMode: resolvedAuth.mode,+ authProvided,+ reason: authResult.reason,+ client: connectParams.client,+ });+ setCloseCause("unauthorized", {+ authMode: resolvedAuth.mode,+ authProvided,+ authReason: authResult.reason,+ allowTailscale: resolvedAuth.allowTailscale,+ client: connectParams.client.id,+ clientDisplayName: connectParams.client.displayName,+ mode: connectParams.client.mode,+ version: connectParams.client.version,+ });+ send({+ type: "res",+ id: frame.id,+ ok: false,+ error: errorShape(ErrorCodes.INVALID_REQUEST, authMessage),+ });+ close(1008, truncateCloseReason(authMessage));+ }; if (!device) {- const canSkipDevice = allowControlUiBypass ? hasSharedAuth : hasTokenAuth;+ const canSkipDevice = sharedAuthOk; if (isControlUi && !allowControlUiBypass) { const errorMessage = "control ui requires HTTPS or localhost (secure context)";@@ -399,8 +454,12 @@ export function attachGatewayWsMessageHandler(params: { return; } - // Allow token-authenticated connections (e.g., control-ui) to skip device identity+ // Allow shared-secret authenticated connections (e.g., control-ui) to skip device identity if (!canSkipDevice) {+ if (!authOk && hasSharedAuth) {+ rejectUnauthorized();+ return;+ } setHandshakeState("failed"); setCloseCause("device-required", { client: connectParams.client.id,@@ -567,15 +626,6 @@ export function attachGatewayWsMessageHandler(params: { } } - const authResult = await authorizeGatewayConnect({- auth: resolvedAuth,- connectAuth: connectParams.auth,- req: upgradeReq,- trustedProxies,- });- let authOk = authResult.ok;- let authMethod =- authResult.method ?? (resolvedAuth.mode === "password" ? "password" : "token"); if (!authOk && connectParams.auth?.token && device) { const tokenCheck = await verifyDeviceToken({ deviceId: device.id,@@ -589,42 +639,11 @@ export function attachGatewayWsMessageHandler(params: { } } if (!authOk) {- setHandshakeState("failed");- logWsControl.warn(- `unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} reason=${authResult.reason ?? "unknown"}`,- );- const authProvided: AuthProvidedKind = connectParams.auth?.token- ? "token"- : connectParams.auth?.password- ? "password"- : "none";- const authMessage = formatGatewayAuthFailureMessage({- authMode: resolvedAuth.mode,- authProvided,- reason: authResult.reason,- client: connectParams.client,- });- setCloseCause("unauthorized", {- authMode: resolvedAuth.mode,- authProvided,- authReason: authResult.reason,AI-assisted change 71568066c763676449bff97b86b80a689208fcf52f9c5d59c9a0114d18fa6478 · Fix b779b4483c036cfa17e99416017eba46922151d8a89993f85d55aba558c298c2