fix(device-pairing): validate callerScopes against resolved token scopes on repair [AI]#72925
fix(device-pairing): validate callerScopes against resolved token scopes on repair [AI]#72925pgondhi987 merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a scope-containment bypass in Confidence Score: 5/5Safe to merge — the fix correctly closes the scope-inheritance bypass without altering any legitimate approval path. The logic change is minimal and targeted: the caller-scope guard is relocated after resolveApprovedTokenScopes() and the two-pass loop ensures atomicity. Both the regression test (forbidden path) and the existing passing test (caller holds sufficient scope) exercise the key branches. No new surface is exposed and no prior auth checks are removed. No files require special attention. Reviews (1): Last reviewed commit: "fix: address issue" | Re-trigger Greptile |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
…pes on repair [AI] (openclaw#72925) * fix: address issue * docs: add changelog entry for PR merge
Summary
approveDevicePairing()checkedcallerScopesonly against operator scopes explicitly inpending.scopes. A re-pair request with empty/omitted scopes skipped the guard entirely, whileresolveApprovedTokenScopes()independently fell back to the existing token's scopes (e.g.operator.admin), minting a fresh token with inherited high-privilege scopes.operator.pairingcould re-pair withpending.scopes = []and obtain a freshoperator.admintoken — violating the scope containment contract documented indocs/channels/pairing.md.callerScopesguard to run afterresolveApprovedTokenScopes(), so it validates the effective (resolved + inherited) scopes that will actually be minted, not just the explicitly requested scopes. Introduced a two-pass loop: validate all roles first, write tokens only if all checks pass.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
callerScopesguard filteredpending.scopesfor operator-prefixed entries before validating caller authority. Whenpending.scopeswas empty,requestedOperatorScopeswas empty and the guard block was never entered. Meanwhile,resolveApprovedTokenScopes()used a separate fallback chain (existingToken.scopes → approvedScopes → existing.scopes) that could returnoperator.adminand other high-privilege scopes without any caller authorization check.Regression Test Plan (if applicable)
src/infra/device-pairing.test.tsoperator.adminbaseline, re-pair with empty scopes, approval caller holds onlyoperator.pairing→ result is{ status: "forbidden", reason: "caller-missing-scope", scope: "operator.admin" }and the existing token is unchanged.User-visible / Behavior Changes
Re-pair approval requests with empty scopes are now rejected with
{ status: "forbidden", reason: "caller-missing-scope" }when the caller'scallerScopesdo not cover the device's inherited operator scopes. Previously these were silently approved. Callers that already hold sufficient scopes to cover the inherited baseline are unaffected.Diagram (if applicable)
Security Impact (required)
forbiddenresult. No legitimate caller should be affected since the guard was always intended to require caller authority for operator scope minting.Repro + Verification
Environment
Steps
operator.adminscope.pending.scopes = undefined).approveDevicePairing(requestId, { callerScopes: ["operator.pairing"] }).Expected
{ status: "forbidden", reason: "caller-missing-scope", scope: "operator.admin" }.Actual (before fix)
{ status: "approved" }and minted a fresh token with["operator.admin", "operator.read", "operator.write"].Evidence
src/infra/device-pairing.test.ts("rejects repair without requested scopes when caller cannot approve inherited token scopes") was written to reflect the vulnerable behavior and passes after the fix.Human Verification (required)
operator.admin, repair with empty scopes); approval path (caller holdsoperator.admin, repair with empty scopes — token preserved correctly per existing test at line 593).caller-scopes-requiredpath (nocallerScopesprovided at all) now also fires for inherited operator scopes.Review Conversations
Compatibility / Migration
forbiddenwhere previously it returnedapprovedfor the vulnerable path. Callers with correct scopes are unaffected.Risks and Mitigations
callerScopeswill now fail.callerScopesparameter was always required for operator scope approval (per the existingcaller-scopes-requiredguard). The only new enforcement is that it also applies when scopes are inherited rather than explicitly requested.