openclaw - ✅(Solved) Fix [Bug]: Setup-code races can revive consumed bootstrap tokens [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#78276Fetched 2026-05-07 03:38:55
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
2
Author
Timeline (top)
commented ×1cross-referenced ×1

Root Cause

This crosses the gateway authentication boundary because resolveConnectAuthDecision() upgrades a connection to authMethod = "bootstrap-token" whenever verifyDeviceBootstrapToken() accepts the setup code (src/gateway/server/ws-connection/auth-context.ts:159-176). openclaw/SECURITY.md and docs/gateway/security/index.md both define gateway auth and paired-node onboarding as in-scope operator trust boundaries. This finding is not covered by the out-of-scope shared-gateway, prompt-injection-only, or trusted-local-state exclusions because the replay comes from a race in OpenClaw's own bootstrap-token store.

Impact

Fix Action

Fixed

PR fix notes

PR #78277: fix: Setup-code races can revive consumed bootstrap tokens

Description (problem / solution / changelog)

Summary

  • Fixes a cross-process lost-update race in devices/bootstrap.json where concurrent setup-code issuance and bootstrap verification could resurrect a consumed setup code.
  • Adds cross-process file locking around bootstrap token issuance, verification, redemption, and revocation.
  • Adds regression coverage for concurrent bootstrap state updates so stale whole-file snapshots cannot revive consumed tokens or erase newer codes.

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 #78276
  • This PR fixes a bug or regression

Root Cause

Bootstrap token state used process-local serialization plus atomic whole-file replacement. That protected callers inside one process, but it did not serialize multiple OpenClaw processes writing the same devices/bootstrap.json file. A stale issuer snapshot could therefore land after a verifier/redeemer update and restore a consumed setup code.

Regression Test Plan

  • Added/updated src/infra/device-bootstrap.test.ts coverage for concurrent bootstrap token writeback.
  • Verified that the touched bootstrap state code lints and typechecks.
  • Verified the broader workflow gate against the fresh upstream baseline; upstream test noise remained baseline-only with no new failures.

User-visible / Behavior Changes

  • Setup-code issuance and redemption now serialize across processes through a file lock.
  • Consumed bootstrap setup codes should stay consumed even when another process concurrently issues a replacement code.
  • No config, migration, or compatibility change is required.

Security Impact

  • New permissions/capabilities? (Yes/No): No
  • Secrets/tokens handling changed? (Yes/No): Yes
  • New/changed network calls? (Yes/No): No
  • Command/tool execution surface changed? (Yes/No): No
  • Data access scope changed? (Yes/No): No
  • If any Yes, explain risk + mitigation: Token state writes now use a cross-process lock so stale bootstrap-token snapshots cannot restore consumed setup codes. Regression tests cover the race, and validation passed with only pre-existing upstream baseline failures.

Repro + Verification

Environment

  • Runtime/container: local OpenClaw managed worktree
  • Model/provider: github-copilot/gpt-5.4 generated the fix; manual recovery removed unrelated AGENTS.md churn and repaired workflow metadata.
  • Relevant config: staged high-security submission workflow with release/drift skips recorded for recovery.

Steps

  1. Reproduce the linked issue by loading setup code A in one process, verifying/redeeming A in another path, and letting a concurrent issuer write an older snapshot last.
  2. Apply this branch.
  3. Run the validation commands below.

Expected

  • Concurrent setup-code issuance cannot restore a consumed bootstrap token.
  • Validation passes for the touched behavior.

Actual

  • Validation status: passed with pre-existing baseline failures only.

Evidence

Validation commands run on the amended branch:

  • pnpm exec oxlint src/infra/device-bootstrap.ts src/infra/device-bootstrap.test.ts
  • pnpm test src/infra/device-bootstrap.test.ts
  • pnpm check
  • Full workflow gate: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test, classified as passed_with_baseline_failures with new_failures: 0.

Results:

  • Changed-file lint: passed, 0 warnings/errors.
  • Targeted regression test: 23 passed in src/infra/device-bootstrap.test.ts.
  • Check/type/lint policy suite: passed.
  • Full test gate had pre-existing upstream baseline failures/no-output stalls only; baseline comparison reported no new failures.

CVSS from linked issue bundle:

  • CVSS v3.1: 7.1 High
  • CVSS v4.0: 7.6 High

Changed files:

  • src/infra/device-bootstrap.ts
  • src/infra/device-bootstrap.test.ts

Human Verification

  • Verified scenarios: stale bootstrap-token whole-file writeback no longer revives a consumed token under concurrent issue/verify operations.
  • Edge cases checked: targeted bootstrap regression behavior, changed-file lint, full pnpm check, and workflow baseline comparison.
  • What you did not verify: A manual multi-process Gateway pairing session outside the automated 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: Bootstrap token operations now wait on a file lock when another process is updating the same state file.
    • Mitigation: The lock is scoped to bootstrap token state and prevents security-relevant stale snapshot writes.
  • Risk: Broader full-suite validation contained upstream baseline stalls.
    • Mitigation: The workflow compared against a fresh upstream baseline and reported new_failures: 0; targeted regression, lint, and pnpm check all passed on the amended branch.

Changed files

  • src/infra/device-bootstrap.test.ts (modified, +49/-0)
  • src/infra/device-bootstrap.ts (modified, +26/-6)

Code Example

export async function issueDeviceBootstrapToken(...) {
  return await withLock(async () => {
    const state = await loadState(params.baseDir);
    state[token] = { token, ts: issuedAtMs, profile, redeemedProfile, issuedAtMs };
    await persistState(state, params.baseDir);
    return { token, expiresAtMs: issuedAtMs + DEVICE_BOOTSTRAP_TOKEN_TTL_MS };
  });
}

export async function verifyDeviceBootstrapToken(...) {
  return await withLock(async () => {
    const state = await loadState(params.baseDir);
    state[tokenKey] = { ...record, profile: allowedProfile, deviceId, publicKey, lastUsedAtMs };
    await persistState(state, params.baseDir);
    return { ok: true };
  });
}
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score7.1 / 10.07.6 / 10.0
SeverityHighHigh
VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:LCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:P/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
CalculatorCVSS v3.1 CalculatorCVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

This crosses the gateway authentication boundary because resolveConnectAuthDecision() upgrades a connection to authMethod = "bootstrap-token" whenever verifyDeviceBootstrapToken() accepts the setup code (src/gateway/server/ws-connection/auth-context.ts:159-176). openclaw/SECURITY.md and docs/gateway/security/index.md both define gateway auth and paired-node onboarding as in-scope operator trust boundaries. This finding is not covered by the out-of-scope shared-gateway, prompt-injection-only, or trusted-local-state exclusions because the replay comes from a race in OpenClaw's own bootstrap-token store.

Impact

A consumed setup code can be written back into devices/bootstrap.json by a concurrent issuer process, making the old code valid again for bootstrap authentication. Anyone holding that stale code can silently pair a fresh node device and inherit the bootstrap handoff roles and scopes, while the same race can also delete a newly issued replacement code.

Affected Component

File: src/infra/device-bootstrap.ts:209-225,343-405

export async function issueDeviceBootstrapToken(...) {
  return await withLock(async () => {
    const state = await loadState(params.baseDir);
    state[token] = { token, ts: issuedAtMs, profile, redeemedProfile, issuedAtMs };
    await persistState(state, params.baseDir);
    return { token, expiresAtMs: issuedAtMs + DEVICE_BOOTSTRAP_TOKEN_TTL_MS };
  });
}

export async function verifyDeviceBootstrapToken(...) {
  return await withLock(async () => {
    const state = await loadState(params.baseDir);
    state[tokenKey] = { ...record, profile: allowedProfile, deviceId, publicKey, lastUsedAtMs };
    await persistState(state, params.baseDir);
    return { ok: true };
  });
}

src/infra/json-files.ts:84-161 provides only process-local createAsyncLock() serialization plus atomic whole-file replacement, while src/gateway/server/ws-connection/auth-context.ts:159-176 and src/gateway/server/ws-connection/message-handler.ts:986-1041,1478-1505 turn a verified bootstrap token into silent node pairing and post-hello-ok revocation.

Technical Reproduction

  1. Generate setup code A with openclaw qr --setup-code-only; the CLI goes through resolvePairingSetupFromConfig() and issueDeviceBootstrapToken() and writes A into devices/bootstrap.json (src/cli/qr-cli.ts:197-214, src/pairing/setup-code.ts:334-385, src/infra/device-bootstrap.ts:201-225).
  2. Start a node handshake that presents A. resolveConnectAuthDecision() calls verifyDeviceBootstrapToken(), which loads the JSON map, binds the token to the connecting device identity, and persists the whole file (src/gateway/server/ws-connection/auth-context.ts:159-176, src/infra/device-bootstrap.ts:335-405). Pause the Gateway after loadState() and before persistState().
  3. From a second process pointed at the same state directory, run openclaw qr --setup-code-only again. Its own issueDeviceBootstrapToken() instance reads the stale pre-bind snapshot and prepares a whole-file write containing unbound A plus new token B.
  4. Resume the first handshake and let it reach hello-ok. The gateway silently approves bootstrap pairing for an unpaired node and only then redeems and revokes the bootstrap token (src/gateway/server/ws-connection/message-handler.ts:986-1041,1478-1505). Let the stale issuer write land last.
  5. devices/bootstrap.json now contains A again as an unbound live record. Reconnect from a different device identity with A; verifyDeviceBootstrapToken() accepts it, resolveConnectAuthDecision() sets authMethod = "bootstrap-token", and the bootstrap-only silent approval path pairs the new node.

Demonstrated Impact

This is a cross-process lost-update bug on an authentication artifact. createAsyncLock() in src/infra/json-files.ts:146-160 serializes only callers inside one process, and writeJsonAtomic() in src/infra/json-files.ts:84-144 replaces the full JSON snapshot without merging concurrent changes. OpenClaw already uses a .lock sidecar file for cross-process pairing-store writes through withFileLock() (src/plugin-sdk/file-lock.ts:166-229, src/pairing/pairing-store.ts:36-45,107-115), but src/infra/device-bootstrap.ts does not.

The revived token is immediately security-relevant in the handshake sink. After verifyDeviceBootstrapToken() succeeds, resolveConnectAuthDecision() authenticates the connection as bootstrap-token, and requirePairing("not-paired") auto-approves an unpaired node when the bound bootstrap profile is present (src/gateway/server/ws-connection/message-handler.ts:949-1041). The same path then provisions device tokens for every role in the bootstrap profile, including operator scopes from PAIRING_SETUP_BOOTSTRAP_PROFILE (src/shared/device-bootstrap-profile.ts:22-25, src/gateway/server/ws-connection/message-handler.ts:1242-1267).

The race also has a second observable effect: whichever writer lands last can delete the other writer's update, so a freshly issued replacement code can disappear before the operator uses it. That contradicts the user-facing guarantee that the setup code is a single-use bootstrap token (extensions/device-pair/index.ts:387-391).

Environment

Re-verified against latest stable release v2026.5.4 (325df3efefe9c0887d9357732e68fc8556e78d79, published 2026-05-05T08:24:01Z) and current main (8a68ea092d0a79e7d95f41f5fe59167a7f37dd92, commit date 2026-05-06T04:50:33Z). In both trees, src/infra/device-bootstrap.ts still protects bootstrap state with only process-local createAsyncLock() around load/persist of devices/bootstrap.json; there is no cross-process withFileLock() or equivalent around these whole-file writes. Reproduction requires a running Gateway process plus a second process issuing openclaw qr --setup-code-only against the same pairing state directory. The mutable state directory defaults to ~/.openclaw and can be overridden with OPENCLAW_STATE_DIR (src/config/paths.ts:55-69).

Remediation Advice

Protect devices/bootstrap.json with the same kind of cross-process lock already used for pairing-store state, or move bootstrap issuance, verification, redemption, and revocation behind one Gateway-owned transactional API. The write path also needs reload-before-commit or compare-and-merge semantics so stale snapshots cannot resurrect revoked tokens, roll back device binding, or erase freshly issued setup codes.

<!-- submission-marker:RE-sdn-setup-code-races-revive-consumed-bootstrap-tokens -->

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

openclaw - ✅(Solved) Fix [Bug]: Setup-code races can revive consumed bootstrap tokens [1 pull requests, 1 comments, 2 participants]