openclaw - ✅(Solved) Fix [Bug]: Device-less operator can self-declare arbitrary scopes including admin via WS connect params [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#71895Fetched 2026-04-27 05:37:40
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
closed ×1commented ×1cross-referenced ×1

Root Cause

Root cause alignment: The clearUnboundScopes guard at message-handler.ts:537 is conditioned on decision.kind !== "allow", but the device-less allow path (roleCanSkipDeviceIdentity) returns kind: "allow" without verifying device identity. This allows client-supplied scopes to survive into client.connect.scopes and be consumed by authorizeGatewayMethod unchecked.

Fix Action

Fix / Workaround

  1. Open a WebSocket connection to the gateway using a valid shared token (no device identity, no paired device). Set connectParams.role = "operator" and connectParams.scopes = ["operator.admin"].
  2. The handshake evaluates evaluateMissingDeviceIdentity: because roleCanSkipDeviceIdentity("operator", true) is true, the function returns { kind: "allow" } at connect-policy.ts:119-120.
  3. message-handler.ts:537 evaluates !device && decision.kind !== "allow" — this is false because decision.kind === "allow", so clearUnboundScopes() is never called.
  4. Send any admin-only gateway request (e.g. session.kill, cron mutation). authorizeGatewayMethod reads client.connect.scopes which contains ["operator.admin"], hits the scopes.includes(ADMIN_SCOPE) branch at line 58, and returns null (authorized).
  5. The request is dispatched with full admin privileges despite the client never having presented device identity.

PR fix notes

PR #71896: fix: Device-less operator can self-declare arbitrary scopes...

Description (problem / solution / changelog)

Summary

  • Problem: An authenticated operator who possesses a valid gateway token or password but has no device identity can supply arbitrary scope values (e.g. ["operator.admin"]) in the WebSocket connect request; because clearUnboundScopes() is guarded by decision.kind !== "allow" and the roleCanSkipDeviceIdentity path returns { kind: "allow" }, the self-declared scopes survive into client.connect.scopes and are consumed unchanged by authorizeGatewayMethod, granting unrestricted access to every admin-gated gateway method.
  • Why it matters: Open a WebSocket connection to the gateway using a valid shared token (no device identity, no paired device).
  • What changed: Open a WebSocket connection to the gateway using a valid shared token (no device identity, no paired device).
  • What did NOT change (scope boundary): No unrelated defaults, migrations, or compatibility behavior were intentionally changed.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #71895
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: Open a WebSocket connection to the gateway using a valid shared token (no device identity, no paired device).
  • Missing detection / guardrail: No narrower detection note was recorded in the issue bundle.
  • Contributing context (if known): See the linked issue analysis and changed files below.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this: Validation command pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test
  • Target test or file: Not explicitly recorded in the issue bundle.
  • Scenario the test should lock in: An authenticated operator who possesses a valid gateway token or password but has no device identity can supply arbitrary scope values (e.g. ["operator.admin"]) in the WebSocket connect request; because clearUnboundScopes() is guarded by decision.kind !== "allow" and the roleCanSkipDeviceIdentity path returns { kind: "allow" }, the self-declared scopes survive into client.connect.scopes and are consumed unchanged by authorizeGatewayMethod, granting unrestricted access to every admin-gated gateway method.
  • Why this is the smallest reliable guardrail: It validates the failing behavior on the touched path without expanding scope.
  • Existing test that already covers this (if any): Unknown.
  • If no new test is added, why not: The staged bundle did not record a narrower regression target.

User-visible / Behavior Changes

  • None beyond resolving the linked issue's broken behavior.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? (Yes/No): Yes
  • Secrets/tokens handling changed? (Yes/No): Yes
  • New/changed network calls? (Yes/No): Yes
  • Command/tool execution surface changed? (Yes/No): Yes
  • Data access scope changed? (Yes/No): Yes
  • If any Yes, explain risk + mitigation: Open a WebSocket connection to the gateway using a valid shared token (no device identity, no paired device).. Mitigation: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test reported failed.

Repro + Verification

Environment

  • OS: N/A
  • Runtime/container: N/A
  • Model/provider: github-copilot/gpt-5.4
  • Integration/channel (if any): N/A
  • Relevant config (redacted): AI-assisted=yes

Steps

  1. Reproduce the linked issue using the recorded issue bundle.
  2. Apply the fix from this branch.
  3. Run pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test.

Expected

  • An authenticated operator who possesses a valid gateway token or password but has no device identity can supply arbitrary scope values (e.g. ["operator.admin"]) in the WebSocket connect request; because clearUnboundScopes() is guarded by decision.kind !== "allow" and the roleCanSkipDeviceIdentity path returns { kind: "allow" }, the self-declared scopes survive into client.connect.scopes and are consumed unchanged by authorizeGatewayMethod, granting unrestricted access to every admin-gated gateway method.
  • Validation passes for the touched behavior.

Actual

  • Validation status: failed

Evidence

  • Validation evidence: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test reported failed.
  • CVSS v3.1: 9.6 (Critical)
  • CVSS v4.0: 9.3 (Critical)
  • Changed files:
  • src/gateway/server/ws-connection/connect-policy.test.ts (+14/-0)
  • src/gateway/server/ws-connection/connect-policy.ts (+18/-11)

Human Verification (required)

  • Verified scenarios: An authenticated operator who possesses a valid gateway token or password but has no device identity can supply arbitrary scope values (e.g. ["operator.admin"]) in the WebSocket connect request; because clearUnboundScopes() is guarded by decision.kind !== "allow" and the roleCanSkipDeviceIdentity path returns { kind: "allow" }, the self-declared scopes survive into client.connect.scopes and are consumed unchanged by authorizeGatewayMethod, granting unrestricted access to every admin-gated gateway method.
  • Edge cases checked: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test completed with status failed.
  • What you did not verify: Interactive/manual scenarios not captured in the staged issue bundle.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No): Yes
  • Config/env changes? (Yes/No): No
  • Migration needed? (Yes/No): No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Regression in the touched behavior while closing the linked issue.
    • Mitigation: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test reported failed and the changed-file scope stayed limited to the recorded diff.

Changed files

  • src/gateway/server/ws-connection/connect-policy.test.ts (modified, +14/-0)
  • src/gateway/server/ws-connection/connect-policy.ts (modified, +18/-11)

Code Example

// Shared token/password auth can bypass pairing for trusted operators.
// Device-less clients only keep self-declared scopes on the explicit
// allow path, including trusted token-authenticated backend operators.
if (!device && decision.kind !== "allow") {
  clearUnboundScopes();
}
if (decision.kind === "allow") {
  return true;
}

---

export function roleCanSkipDeviceIdentity(role: GatewayRole, sharedAuthOk: boolean): boolean {
  return role === "operator" && sharedAuthOk;
}

---

const scopes = client.connect.scopes ?? [];
// ...
if (scopes.includes(ADMIN_SCOPE)) {
  return null;   // unconditional allow for any method
}
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score9.6 / 10.09.3 / 10.0
SeverityCriticalCritical
VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:NCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N
CalculatorCVSS v3.1 CalculatorCVSS v4.0 Calculator

Threat Model Alignment

ControlExpectedObservedStatus
Operator role requires device identity for elevated scopesDevice identity must be verified before admin scopes are grantedroleCanSkipDeviceIdentity bypasses device check for operator role with shared authFAIL — operator can self-declare admin scopes without device
Scope whitelist enforcementServer validates scopes against authorized grantsauthorizeGatewayMethod accepts client-supplied scopes verbatim including ADMIN_SCOPEFAIL — no server-side scope whitelist
Untrusted origin rejectionUntrusted origins cannot supply valid gateway credentialsValid token + operator role is sufficient; no origin/device bindingFAIL — token possession alone grants admin access

Root cause alignment: The clearUnboundScopes guard at message-handler.ts:537 is conditioned on decision.kind !== "allow", but the device-less allow path (roleCanSkipDeviceIdentity) returns kind: "allow" without verifying device identity. This allows client-supplied scopes to survive into client.connect.scopes and be consumed by authorizeGatewayMethod unchecked.

Classification: security-specific

Control gap: A device-less operator with a valid shared token can declare arbitrary scopes including operator.admin via WS connect params. The server does not cross-reference self-declared scopes against a server-side whitelist keyed to the authenticated identity. This collapses the privilege boundary for all admin-gated gateway methods (session management, cron mutations, exec approvals) to a shared-token possession check.

Impact

An authenticated operator who possesses a valid gateway token or password but has no device identity can supply arbitrary scope values (e.g. ["operator.admin"]) in the WebSocket connect request; because clearUnboundScopes() is guarded by decision.kind !== "allow" and the roleCanSkipDeviceIdentity path returns { kind: "allow" }, the self-declared scopes survive into client.connect.scopes and are consumed unchanged by authorizeGatewayMethod, granting unrestricted access to every admin-gated gateway method.

Affected Component

File: openclaw/src/gateway/server/ws-connection/message-handler.ts:537

// Shared token/password auth can bypass pairing for trusted operators.
// Device-less clients only keep self-declared scopes on the explicit
// allow path, including trusted token-authenticated backend operators.
if (!device && decision.kind !== "allow") {
  clearUnboundScopes();
}
if (decision.kind === "allow") {
  return true;
}

File: openclaw/src/gateway/role-policy.ts:14

export function roleCanSkipDeviceIdentity(role: GatewayRole, sharedAuthOk: boolean): boolean {
  return role === "operator" && sharedAuthOk;
}

File: openclaw/src/gateway/server-methods.ts:51

const scopes = client.connect.scopes ?? [];
// ...
if (scopes.includes(ADMIN_SCOPE)) {
  return null;   // unconditional allow for any method
}

Technical Reproduction

  1. Open a WebSocket connection to the gateway using a valid shared token (no device identity, no paired device). Set connectParams.role = "operator" and connectParams.scopes = ["operator.admin"].
  2. The handshake evaluates evaluateMissingDeviceIdentity: because roleCanSkipDeviceIdentity("operator", true) is true, the function returns { kind: "allow" } at connect-policy.ts:119-120.
  3. message-handler.ts:537 evaluates !device && decision.kind !== "allow" — this is false because decision.kind === "allow", so clearUnboundScopes() is never called.
  4. Send any admin-only gateway request (e.g. session.kill, cron mutation). authorizeGatewayMethod reads client.connect.scopes which contains ["operator.admin"], hits the scopes.includes(ADMIN_SCOPE) branch at line 58, and returns null (authorized).
  5. The request is dispatched with full admin privileges despite the client never having presented device identity.

Demonstrated Impact

Root cause: the clearUnboundScopes guard is inverted — it fires on decision.kind !== "allow" (i.e., the reject branches) but not on the allow branch that is specifically triggered for device-less operators authenticated via shared secret. The comment in the code ("Device-less clients only keep self-declared scopes on the explicit allow path") documents this as intentional, but the consequence is that any client with a valid token can self-promote to admin by populating the scopes field before connecting.

Existing controls do not prevent this: the operator role check at server-methods.ts:52 passes because the client role is "operator", and the scope check at line 58 passes because ["operator.admin"] is client-supplied. There is no server-side scope validation step that cross-references a scope whitelist against what the authenticated identity is actually entitled to.

Trust boundary impact: the gateway's admin operation surface (session management, cron mutations, exec approvals) is protected only by scope gating. Self-declared scopes collapse that gate to a shared-token possession check, reducing privilege escalation to any party who holds a valid gateway token/password.

Environment

Verified against v2026.4.24 (commit 6507387f433deb0e7beb22abb4625a40f3b6b97e, published 2026-04-25T18:15:17Z). Observed in gateway source at openclaw/src/gateway/. Reproducible with any gateway configuration where a shared token or password is set and the Control UI or any operator client can connect.

Remediation Advice

Scopes for device-less operator connections should be bounded server-side against a whitelist derived from the authentication method and role, not taken verbatim from client-supplied connect params. clearUnboundScopes() should be called unconditionally when device identity is absent, regardless of whether the decision is "allow", unless the specific scopes have been independently verified through a server-controlled grant mechanism.

<!-- submission-marker:AA-rem-operator-scope-self-declaration-bypass -->

extent analysis

TL;DR

The most likely fix is to modify the clearUnboundScopes guard to call unconditionally when device identity is absent, ensuring that self-declared scopes are cleared unless independently verified through a server-controlled grant mechanism.

Guidance

  • Review the message-handler.ts file and modify the clearUnboundScopes guard to call unconditionally when device is falsy, regardless of the decision.kind value.
  • Implement a server-side scope validation step that cross-references a scope whitelist against the authenticated identity's entitlements.
  • Update the roleCanSkipDeviceIdentity function to consider the implications of allowing device-less operators to self-declare scopes.
  • Verify that the fix works by testing the gateway with a device-less operator connection and attempting to self-declare arbitrary scopes.

Example

// Modified clearUnboundScopes guard
if (!device) {
  clearUnboundScopes();
}

Notes

The provided fix assumes that the clearUnboundScopes function is correctly implemented and effective in clearing self-declared scopes. Additional testing and verification may be necessary to ensure the fix is comprehensive and does not introduce unintended consequences.

Recommendation

Apply the workaround by modifying the clearUnboundScopes guard and implementing server-side scope validation, as this directly addresses the identified security vulnerability and prevents self-declared scope bypass.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING