openclaw - ✅(Solved) Fix [Bug]: Implicit latest-device approval can pair the wrong requester [1 pull requests, 1 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#64158Fetched 2026-04-11 06:16:13
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
closed ×1cross-referenced ×1

Root Cause

docs/web/control-ui.md:40-55 and docs/concepts/architecture.md:101-109 say new non-local device IDs still require explicit pairing approval. SECURITY.md also states that pairing a node grants operator-level remote capability on that node, so the pairing record is a real authorization boundary rather than UI-only metadata. This bug lets requester B consume the operator's approval that was intended for requester A by manipulating the implicit latest-request selector. It is not covered by the Out of Scope section because it does not rely on hostile multi-tenant assumptions, prompt injection, or write access to trusted local state.

Impact

An attacker who can submit a pending device-pairing request can keep that request newest and cause openclaw devices approve or openclaw devices approve --latest to approve the attacker's device instead of the operator's intended one. For browser/operator flows this can mint a full operator device token, and for node flows it can pair an unintended remote execution endpoint.

Fix Action

Fixed

PR fix notes

PR #64160: fix: Implicit latest-device approval can pair the wrong requester

Description (problem / solution / changelog)

Fix Summary

An attacker who can submit a pending device-pairing request can keep that request newest and cause openclaw devices approve or openclaw devices approve --latest to approve the attacker's device instead of the operator's intended one. For browser/operator flows this can mint a full operator device token, and for node flows it can pair an unintended remote execution endpoint.

Issue Linkage

Fixes #64158

Security Snapshot

  • CVSS v3.1: 9.0 (Critical)
  • CVSS v4.0: 9.4 (Critical)

Implementation Details

Files Changed

  • src/cli/devices-cli.test.ts (+36/-5)
  • src/cli/devices-cli.ts (+34/-3)
  • src/infra/device-pairing.test.ts (+31/-0)
  • src/infra/device-pairing.ts (+4/-1)

Technical Analysis

  1. Create a legitimate non-local device-pairing request A (for example a new Control UI browser session or node). OpenClaw documents that non-local device IDs still require explicit approval before the connection is trusted.
  2. Create attacker request B for a different device. src/gateway/server/ws-connection/message-handler.ts:871-880 forwards the requester-controlled role, scopes, and device metadata into requestDevicePairing().
  3. Reconnect the attacker device before the operator approves anything. Because the attacker reuses the same public key, role, and scope set, samePendingApprovalSnapshot() and refreshPendingDevicePairingRequest() keep B's requestId but refresh ts to Date.now().
  4. The refresh is silent. message-handler.ts:938-939 only broadcasts device.pair.requested when pairing.created is true, so the operator receives no new approval event when B merely refreshes recency.
  5. Have the operator run openclaw devices approve or openclaw devices approve --latest. src/cli/devices-cli.ts:407-408 reloads pending requests at approval time and chooses the entry with the greatest ts.
  6. That attacker request now sorts first in listDevicePairing() and remains the value selected by selectLatestPendingRequest(), so approveDevicePairing(latest.requestId, { callerScopes: ["operator.admin", "operator.pairing"] }) approves the attacker-controlled device rather than the intended requester.

Validation Evidence

  • Command: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test
  • Status: passed (with pre-existing baseline failures)

Risk and Compatibility

non-breaking; no known regression impact

AI-Assisted Disclosure

  • AI-assisted: yes
  • Model: opencode/claude-sonnet-4.6

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • docs/cli/devices.md (modified, +5/-3)
  • docs/cli/index.md (modified, +1/-1)
  • src/auto-reply/reply/commands-compact.ts (modified, +6/-7)
  • src/auto-reply/reply/commands-models.test.ts (modified, +6/-2)
  • src/auto-reply/reply/commands-models.ts (modified, +6/-2)
  • src/auto-reply/reply/commands-system-prompt.test.ts (modified, +2/-0)
  • src/cli/devices-cli.test.ts (modified, +117/-20)
  • src/cli/devices-cli.ts (modified, +96/-6)
  • src/cron/service/ops.regression.test.ts (modified, +39/-34)
  • src/cron/service/timer.regression.test.ts (modified, +4/-4)
  • src/infra/device-pairing.test.ts (modified, +31/-0)
  • src/infra/device-pairing.ts (modified, +4/-1)

Code Example

// src/cli/devices-cli.ts
const latest = selectLatestPendingRequest((await listPairingWithFallback(opts)).pending);
resolvedRequestId = latest?.requestId?.trim();

// src/infra/device-pairing.ts
return {
  ...existing,
  // same requestId, refreshed recency
  ts: Date.now(),
};

// src/gateway/server/ws-connection/message-handler.ts
} else if (pairing.created) {
  context.broadcast("device.pair.requested", pairing.request, { dropIfSlow: true });
}

// ui/src/ui/gateway.ts
export const CONTROL_UI_OPERATOR_SCOPES = [
  "operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing",
];
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

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

Threat Model Alignment

Classification: security-specific

docs/web/control-ui.md:40-55 and docs/concepts/architecture.md:101-109 say new non-local device IDs still require explicit pairing approval. SECURITY.md also states that pairing a node grants operator-level remote capability on that node, so the pairing record is a real authorization boundary rather than UI-only metadata. This bug lets requester B consume the operator's approval that was intended for requester A by manipulating the implicit latest-request selector. It is not covered by the Out of Scope section because it does not rely on hostile multi-tenant assumptions, prompt injection, or write access to trusted local state.

Impact

An attacker who can submit a pending device-pairing request can keep that request newest and cause openclaw devices approve or openclaw devices approve --latest to approve the attacker's device instead of the operator's intended one. For browser/operator flows this can mint a full operator device token, and for node flows it can pair an unintended remote execution endpoint.

Affected Component

Files: src/cli/devices-cli.ts:193-201,400-427, src/infra/device-pairing.ts:257-295,416-422,441-466,499-595, src/gateway/server/ws-connection/message-handler.ts:891-900,955-959, ui/src/ui/gateway.ts:132-140,386-388

// src/cli/devices-cli.ts
const latest = selectLatestPendingRequest((await listPairingWithFallback(opts)).pending);
resolvedRequestId = latest?.requestId?.trim();

// src/infra/device-pairing.ts
return {
  ...existing,
  // same requestId, refreshed recency
  ts: Date.now(),
};

// src/gateway/server/ws-connection/message-handler.ts
} else if (pairing.created) {
  context.broadcast("device.pair.requested", pairing.request, { dropIfSlow: true });
}

// ui/src/ui/gateway.ts
export const CONTROL_UI_OPERATOR_SCOPES = [
  "operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing",
];

Technical Reproduction

  1. Create a legitimate non-local device-pairing request A (for example a new Control UI browser session or node). OpenClaw documents that non-local device IDs still require explicit approval before the connection is trusted.
  2. Create attacker request B for a different device. src/gateway/server/ws-connection/message-handler.ts:871-880 forwards the requester-controlled role, scopes, and device metadata into requestDevicePairing().
  3. Reconnect the attacker device before the operator approves anything. Because the attacker reuses the same public key, role, and scope set, samePendingApprovalSnapshot() and refreshPendingDevicePairingRequest() keep B's requestId but refresh ts to Date.now().
  4. The refresh is silent. message-handler.ts:938-939 only broadcasts device.pair.requested when pairing.created is true, so the operator receives no new approval event when B merely refreshes recency.
  5. Have the operator run openclaw devices approve or openclaw devices approve --latest. src/cli/devices-cli.ts:407-408 reloads pending requests at approval time and chooses the entry with the greatest ts.
  6. That attacker request now sorts first in listDevicePairing() and remains the value selected by selectLatestPendingRequest(), so approveDevicePairing(latest.requestId, { callerScopes: ["operator.admin", "operator.pairing"] }) approves the attacker-controlled device rather than the intended requester.

Demonstrated Impact

This is a concrete approval-confusion bug, not just stale UI state. listDevicePairing() sorts pending requests by descending ts, selectLatestPendingRequest() repeats that winner selection, and the CLI approves the chosen request immediately without a confirmation screen that re-identifies the device, role, scopes, or remote IP. The risky path is still surfaced as normal operator guidance in docs/cli/devices.md:53,63, src/commands/status.command-sections.ts:373, openclaw/skills/node-connect/SKILL.md:107, and apps/android/README.md:259,285.

Once the wrong request wins, approveDevicePairing() copies the attacker-chosen roles and scopes into the paired record's approvedScopes set and mints fresh tokens from that pending entry. The official Control UI asks for operator.admin, operator.read, operator.write, operator.approvals, and operator.pairing by default (ui/src/ui/gateway.ts:131-139,385-388), while docs/web/control-ui.md:40-55 describes this pairing step as protection against unauthorized access. For node requests, SECURITY.md says pairing a node grants operator-level remote capability on that node. The authorization decision therefore lands on the wrong device, with full downstream confidentiality, integrity, and availability impact.

Environment

Re-verified against OpenClaw release v2026.4.9 (60db001e96ff96fe8796b3a9ff1b93125cceaa2e, published 2026-04-09T02:25:28Z) by reviewing the released source paths above, including the current CLI approval flow, pairing-state refresh logic, websocket request notification path, and the latest operator-facing docs that still recommend openclaw devices approve --latest. The bug affects any deployment where an operator uses the implicit latest-approval workflow for a non-local device-pairing request (Control UI/browser, node, or another WS client that still requires explicit pairing approval).

Remediation Advice

Remove implicit newest-request approval for non-local device pairing, or require an explicit confirmation step that shows the exact requestId, device identity, requested role, scopes, and remote IP immediately before approval. Reconnect refreshes that keep the same requestId should not silently change approval priority; they should either preserve original queue order or force a visible re-selection by the operator.

<!-- submission-marker:BL-sce-latest-device-approve-race-2 -->

extent analysis

TL;DR

To fix the approval-confusion bug, remove implicit newest-request approval for non-local device pairing or require an explicit confirmation step that shows the exact request details before approval.

Guidance

  1. Modify the approval flow: Change the openclaw devices approve and openclaw devices approve --latest commands to require an explicit confirmation step, displaying the requestId, device identity, requested role, scopes, and remote IP.
  2. Update the pairing-state refresh logic: Modify the refreshPendingDevicePairingRequest() function to preserve the original queue order or force a visible re-selection by the operator when a reconnect refreshes a pending request.
  3. Enhance the websocket request notification path: Update the message-handler.ts file to broadcast a new approval event when a reconnect refreshes a pending request, ensuring the operator receives a notification.
  4. Review and update documentation: Ensure that the official documentation, including docs/cli/devices.md, docs/web/control-ui.md, and SECURITY.md, reflects the updated approval flow and pairing-state refresh logic.

Example

// Example of an explicit confirmation step
const confirmApproval = async (requestId: string, deviceIdentity: string, role: string, scopes: string[], remoteIP: string) => {
  // Display the request details to the operator
  console.log(`Approve device pairing request ${requestId} for ${deviceIdentity} with role ${role} and scopes ${scopes} from ${remoteIP}?`);
  // Wait for operator confirmation
  const confirmed = await promptOperator();
  if (confirmed) {
    // Approve the device pairing request
    approveDevicePairing(requestId, { callerScopes: ["operator.admin", "operator.pairing"] });
  } else {
    // Reject the device pairing request
    rejectDevicePairing(requestId);
  }
};

Notes

The provided solution focuses on removing the implicit newest-request approval and requiring an explicit confirmation step. However, the exact implementation details may vary depending on the specific requirements and constraints of the OpenClaw system.

Recommendation

Apply the workaround by modifying the approval flow and pairing-state refresh logic to require an explicit confirmation step, as this will prevent the approval-confusion bug and ensure the security of the system.

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