Repository navigation
security: bind view-state ids to a host-minted claim token (cross-view secret disclosure) #2151
Description
Context
The vps2 view-state series lets the webview send its own viewStateId in webviewDidLaunch; the extension adopts it in ClineProvider.setViewStateId and loads that view's persisted viewStates entry (including provider profile secrets read from context.secrets) into the state posted back to the webview.
setViewStateId only refuses ids that are still held by a LIVE instance (getAllInstances over activeInstances). dispose() removes the instance but leaves the durable viewStates entry in place (clearPersistedViewState only runs in the reset path), so a compromised webview can adopt a closed view's id and receive that view's secrets. This is the Security Boundaries error on #1919, inherited from the series' replayed units (author: the store in F1a #1546 and the launch adoption in F1c #1552; 7 replayed units carry the adoption path).
Filed separately so the fix lands once in the owner unit and the replayed branches converge by refresh, instead of seven competing patches.
Proposed design (for maintainer review)
- Durable
viewStatesentries gain an optional host-mintedclaimToken(e.g.crypto.randomUUID()), never generated or chosen by the webview. setViewStateIdrejects an id whose entry has no claim token or whose presented token does not match, BEFOREloadViewState/re-keying persisted state.- The launch protocol carries the claim back: the extension messages the webview with
viewStateClaimand the webview only echoes it; a webview cannot mint one. dispose()either revokes the entry's claim (invalidating stale ids) or the claim dies with the host instance, whichever is cheaper to reason about.
Acceptance criteria
- A recovery-style binding (
claimToken) is stored with the durable viewStates entry; it is host-minted and never accepted from webview input. -
setViewStateIdfails closed for an unknown/unbound/mismatched id before any persisted state is loaded. - Regression test: dispose a provider's view, then attempt adoption of that id from a different provider/webview — the adoption is rejected and no secrets are posted.
- Live sibling views keep working (the existing live-claimant test remains green).
- The replayed units carry no second implementation: they inherit via refresh once feat(provider): persist per-view view-state identity and durable viewStates #1546/fix(webview): send stable view-state id on launch and re-pin per-view state #1552 land.
References
- test(webview): F2a - profile-mutation state semantics and the mode-rollback guard #1919 (finding surfaced here; inherited), feat(provider): persist per-view view-state identity and durable viewStates #1546 (store/
setViewStateId), fix(webview): send stable view-state id on launch and re-pin per-view state #1552 (launch adoption) - Related registration pattern: guardedWrite: evaluate descriptor-relative publication (openat/renameat) for containment races #2148 (descriptor-relative publication), guardedWrite: cross-process conditional publication (residual check-to-rename window) #1414
Activity
fix(provider): bind durable view-states entries to a host-minted clai…