#20089: fix(gateway): preserve control-ui scopes when dangerouslyDisableDeviceAuth is set
gateway
size: XS
Cluster:
Device Auth and Security Fixes
## 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
#17605: fix: preserve scopes when disableControlUiDeviceAuth is enabled
by MisterGuy420 · 2026-02-16
91.2%
#17572: fix: make dangerouslyDisableDeviceAuth bypass device identity checks
by gitwithuli · 2026-02-15
88.9%
#17753: fix: Control UI unusable over HTTP - missing scopes
by MisterGuy420 · 2026-02-16
88.6%
#23361: Gateway: reject scope assertions without identity binding
by bmendonca3 · 2026-02-22
87.3%
#17378: fix(gateway): allow dangerouslyDisableDeviceAuth with trusted-proxy...
by ar-nadeem · 2026-02-15
84.7%
#12802: fix(gateway): default unscoped operator connections to read-only
by yubrew · 2026-02-09
83.3%
#17205: fix: enforce full operator scopes for Control UI and Webchat auto-p...
by Limitless2023 · 2026-02-15
83.0%
#17195: fix: Add operator.read/write scopes to Dashboard auto-pairing
by MisterGuy420 · 2026-02-15
82.8%
#23364: Gateway: add risk-ack interlock for dangerous Control UI flags
by bmendonca3 · 2026-02-22
82.5%
#19389: Fix #2248: Allow insecure auth bypass when device signature validat...
by cedillarack · 2026-02-17
82.0%