openclaw - ✅(Solved) Fix [Bug]: node.invoke stays reachable before node pairing approval exists [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#65168Fetched 2026-04-12 13:25:13
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

Root Cause

openclaw/SECURITY.md and docs/gateway/security/index.md define node pairing as the trust step that grants operator-level remote capability on a node, and docs/gateway/pairing.md says node commands are disabled until that approval exists. The implementation crosses that boundary because a merely operator.write caller can invoke commands on a reconnecting node even while node.pair.approve still requires operator.pairing or operator.admin for the same node surface. This is not an out-of-scope report about an intentional trusted-operator primitive: it is a concrete bypass of the documented node-pairing gate that should establish node trust before node.invoke can execute.

Fix Action

Fixed

PR fix notes

PR #65169: fix: node.invoke stays reachable before node pairing approval exists

Description (problem / solution / changelog)

Fix Summary

An authenticated operator.write session can invoke commands on a reconnecting node before node.pair.approve has established that node as trusted. On macOS and Linux, this exposes pre-approval system.run, which can become remote code execution on the node host when the node's local exec-approval policy is permissive.

Issue Linkage

Fixes #65168

Security Snapshot

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

Implementation Details

Files Changed

  • src/gateway/node-connect-reconcile.ts (+1/-1)
  • src/gateway/server.node-invoke-approval-bypass.test.ts (+10/-0)
  • src/gateway/server.roles-allowlist-update.test.ts (+11/-2)

Technical Analysis

  1. Approve device pairing for a node identity, but do not approve the pending node.pair request for that node.
  2. Reconnect the node while it declares allowlisted commands such as canvas.snapshot and system.run.
  3. During the reconnect handshake, reconcileNodePairingOnConnect() sees pairedNode === null, creates a pending node-pair request, and still returns effectiveCommands: declared from src/gateway/node-connect-reconcile.ts:73-86.
  4. src/gateway/server/ws-connection/message-handler.ts:1143-1157 writes those commands into connectParams.commands, and src/gateway/node-registry.ts:45-82 stores them on the connected NodeSession.
  5. From a separate gateway session that has operator.write but not operator.pairing, call node.invoke for system.run or another declared command.
  6. src/gateway/server-methods/nodes.ts:993-1034 checks only the node session's declared commands plus the platform allowlist, then forwards the request to the node. src/gateway/server.node-pairing-authz.test.ts:86-127 confirms that the same caller lacks permission to approve the node pairing, so invocation succeeds before the approval gate it is supposed to depend on.

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: github-copilot/gpt-5.4

Changed files

  • src/gateway/node-connect-reconcile.ts (modified, +1/-1)
  • src/gateway/server.node-invoke-approval-bypass.test.ts (modified, +10/-0)
  • src/gateway/server.roles-allowlist-update.test.ts (modified, +11/-2)

Code Example

// src/gateway/node-connect-reconcile.ts
if (!params.pairedNode) {
  const pendingPairing = await params.requestPairing(
    buildNodePairingRequestInput({
      nodeId,
      connectParams: params.connectParams,
      commands: declared,
      remoteIp: params.reportedClientIp,
    }),
  );
  return {
    nodeId,
    effectiveCommands: declared,
    pendingPairing,
  };
}
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score9.9 / 10.09.4 / 10.0
SeverityCriticalCritical
VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:HCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/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

openclaw/SECURITY.md and docs/gateway/security/index.md define node pairing as the trust step that grants operator-level remote capability on a node, and docs/gateway/pairing.md says node commands are disabled until that approval exists. The implementation crosses that boundary because a merely operator.write caller can invoke commands on a reconnecting node even while node.pair.approve still requires operator.pairing or operator.admin for the same node surface. This is not an out-of-scope report about an intentional trusted-operator primitive: it is a concrete bypass of the documented node-pairing gate that should establish node trust before node.invoke can execute.

Impact

An authenticated operator.write session can invoke commands on a reconnecting node before node.pair.approve has established that node as trusted. On macOS and Linux, this exposes pre-approval system.run, which can become remote code execution on the node host when the node's local exec-approval policy is permissive.

Affected Component

Files:

  • src/gateway/node-connect-reconcile.ts:73-86
  • src/gateway/server/ws-connection/message-handler.ts:1143-1157
  • src/gateway/node-registry.ts:45-82
  • src/gateway/server-methods/nodes.ts:993-1034
  • src/infra/node-pairing-authz.ts:9-20
// src/gateway/node-connect-reconcile.ts
if (!params.pairedNode) {
  const pendingPairing = await params.requestPairing(
    buildNodePairingRequestInput({
      nodeId,
      connectParams: params.connectParams,
      commands: declared,
      remoteIp: params.reportedClientIp,
    }),
  );
  return {
    nodeId,
    effectiveCommands: declared,
    pendingPairing,
  };
}

reconcileNodePairingOnConnect() returns the node's allowlisted declared commands even when getPairedNode(...) returned null. message-handler.ts copies those commands into connectParams.commands, NodeRegistry.register() persists them on the live NodeSession, and node.invoke authorizes execution from that live command list without consulting node-pair approval state. Meanwhile resolveNodePairApprovalScopes() requires operator.pairing plus operator.admin for system.run, so the connect path exposes a stronger surface than the approval path.

Technical Reproduction

  1. Approve device pairing for a node identity, but do not approve the pending node.pair request for that node.
  2. Reconnect the node while it declares allowlisted commands such as canvas.snapshot and system.run.
  3. During the reconnect handshake, reconcileNodePairingOnConnect() sees pairedNode === null, creates a pending node-pair request, and still returns effectiveCommands: declared from src/gateway/node-connect-reconcile.ts:73-86.
  4. src/gateway/server/ws-connection/message-handler.ts:1143-1157 writes those commands into connectParams.commands, and src/gateway/node-registry.ts:45-82 stores them on the connected NodeSession.
  5. From a separate gateway session that has operator.write but not operator.pairing, call node.invoke for system.run or another declared command.
  6. src/gateway/server-methods/nodes.ts:993-1034 checks only the node session's declared commands plus the platform allowlist, then forwards the request to the node. src/gateway/server.node-pairing-authz.test.ts:86-127 confirms that the same caller lacks permission to approve the node pairing, so invocation succeeds before the approval gate it is supposed to depend on.

Demonstrated Impact

The root cause is a fail-open reconnect path for unpaired nodes. src/gateway/node-connect-reconcile.ts:73-86 intentionally records the pending pairing request, but it does not suppress the declared commands for that unpaired state. src/gateway/server-methods/nodes.ts:993-1034 never checks getPairedNode(...), pending pairing records, or required approval scopes; it only checks the live NodeSession.commands array and the global allowlist from src/gateway/node-command-policy.ts:176-240.

That behavior defeats the documented 2026.3.31 gate in docs/gateway/pairing.md:90-102 and the matching changelog entry in CHANGELOG.md:817-826. The shipped regression test src/gateway/server.roles-allowlist-update.test.ts:384-437 also codifies the vulnerable behavior by asserting that declared commands stay available before node pairing exists. On macOS and Linux, the default allowlist in src/gateway/node-command-policy.ts:103-116 includes system.run; if the node's own exec approval setting is security="full" and ask="off", an operator.write caller can execute commands on the node host before any node.pair.approve review occurs.

Environment

Verified against OpenClaw release v2026.4.11 (9863c7ed93f79ca2333e743201bedbbdfbc211dc, published 2026-04-12T00:18:03Z) in the current workspace. Reproduction requires a reconnecting node with approved device pairing, an outstanding unapproved node.pair request, and a separate authenticated gateway session with operator.write scope.

Remediation Advice

Fail closed when a reconnecting node does not yet have an approved node-pair record: keep the connection alive for pairing UX if desired, but publish an empty command set until node.pair.approve succeeds. The node.invoke path should also enforce approved node-pair state directly so command execution cannot depend solely on live reconnect metadata.

<!-- submission-marker:AA-kme-node-invoke-before-node-pairing -->

extent analysis

TL;DR

The most likely fix is to modify the reconcileNodePairingOnConnect() function to return an empty command set when the node is not yet paired, ensuring that commands are not executed before node pairing approval.

Guidance

  • Review the reconcileNodePairingOnConnect() function in src/gateway/node-connect-reconcile.ts to ensure it returns an empty command set when pairedNode is null.
  • Modify the message-handler.ts to not copy commands into connectParams.commands when the node is not paired.
  • Update the node.invoke path in src/gateway/server-methods/nodes.ts to check for approved node-pair state before executing commands.
  • Consider adding a regression test to ensure the fix does not introduce new vulnerabilities.

Example

// src/gateway/node-connect-reconcile.ts
if (!params.pairedNode) {
  // Return an empty command set when the node is not paired
  return {
    nodeId,
    effectiveCommands: [],
    pendingPairing,
  };
}

Notes

The provided fix assumes that the reconcileNodePairingOnConnect() function is the root cause of the issue. However, a thorough review of the codebase and testing is necessary to ensure that the fix does not introduce new vulnerabilities or unintended behavior.

Recommendation

Apply the workaround by modifying the reconcileNodePairingOnConnect() function to return an empty command set when the node is not yet paired, and update the node.invoke path to check for approved node-pair state before executing commands. This will prevent commands from being executed before node pairing approval, addressing the security vulnerability.

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