← Back to PRs

#20089: fix(gateway): preserve control-ui scopes when dangerouslyDisableDeviceAuth is set

by vashkartik open 2026-02-18 14:19 View on GitHub →
gateway size: XS
## Problem When a control-UI client connects using shared authentication and `dangerouslyDisableDeviceAuth: true` (or `allowInsecureAuth: true`) is set in the gateway config, the gateway clears all requested scopes even though the bypass was explicitly granted by the operator. This breaks control-UI clients that connect without device identity — such as local dashboards or operator terminals using a shared secret. Every gateway method returns `missing scope`, making the connection useless. ## Root Cause In `message-handler.ts`, the scope-clearing logic: ```typescript if (scopes.length > 0 && !allowControlUiBypass) { scopes = []; connectParams.scopes = scopes; } ``` `allowControlUiBypass` controls whether the client can skip the secure context check — but it does **not** signal that the operator has explicitly authorized this connection. That authorization comes from `dangerouslyDisableDeviceAuth` or `allowInsecureAuth`. The scope-clearing should key off those flags, not `allowControlUiBypass`. ## Fix Introduce `allowControlUiScopesWithoutDevice` to correctly identify when scope preservation is appropriate: ```typescript const allowControlUiScopesWithoutDevice = isControlUi && sharedAuthOk && (configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true || configSnapshot.gateway?.controlUi?.allowInsecureAuth === true); if (scopes.length > 0 && !allowControlUiScopesWithoutDevice) { scopes = []; connectParams.scopes = scopes; } ``` This preserves the operator's intent: if you've explicitly set `dangerouslyDisableDeviceAuth` and the client authenticates with a valid shared secret, their requested scopes should be honored. ## Reproduction 1. Set `gateway.controlUi.dangerouslyDisableDeviceAuth: true` in `openclaw.json` 2. Connect a control-UI client without device identity (shared secret auth only) 3. Issue any gateway method → `missing scope` error ## After Fix Control-UI clients with shared auth + operator bypass can use their requested scopes normally. --- *Found while running a custom control-UI dashboard against the gateway. Happy to add a test if the team can point me to the relevant test file.* <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR fixes a security gap in the gateway WebSocket handshake where control-UI scopes were preserved without verifying shared-secret authentication when `dangerouslyDisableDeviceAuth` or `allowInsecureAuth` was set. The fix introduces `allowControlUiScopesWithoutDevice`, which adds `sharedAuthOk` as a required condition before allowing scopes to survive the no-device path. This ensures self-declared scopes cannot be kept without proper authentication. - **Security fix**: Scopes are now cleared for device-less control-UI connections unless shared-secret auth succeeds, closing a path where unauthenticated clients could retain self-declared permissions. - **Import reordering**: Type-only imports moved to the top of the import block (cosmetic, no behavioral change). - **No new tests**: The existing e2e test (`"allows control ui with stale device identity when device auth is disabled"`) exercises the new code path but does not explicitly assert that scopes are preserved. Consider adding an assertion on the preserved scopes to prevent future regressions. <h3>Confidence Score: 4/5</h3> - This PR is a targeted security hardening change that is safe to merge with minor style feedback. - The logic change is small, well-scoped, and strictly tightens the existing condition by adding a `sharedAuthOk` requirement. The import reordering is cosmetic. The only concern is the duplicated config flag reads (style, not correctness) and the lack of an explicit scope-preservation assertion in the test suite. - No files require special attention; `src/gateway/server/ws-connection/message-handler.ts` is the only changed file and the logic is straightforward. <sub>Last reviewed commit: ca78c69</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs