Severity: Medium
Area: Web — OAuth / replay
Location
web/src/lib/oauth-state.ts:60-163
Problem
The OAuth state is HMAC-signed but carries only a random nonce that is never recorded or checked server-side, and has no timestamp/TTL. A signed state (and, for native-MCP, the embedded PKCE verifier + DCR client_id) is therefore valid forever and reusable.
Why it matters
Combined with callbacks that only require session.user.id === state.userId, a leaked callback URL (referer header, logs, browser history) can be replayed by the same user indefinitely.
Suggested fix
Add an issued-at timestamp and reject states older than a few minutes. Ideally bind a one-time nonce stored server-side or in a short-lived cookie so each state is single-use.
Severity: Medium
Area: Web — OAuth / replay
Location
web/src/lib/oauth-state.ts:60-163Problem
The OAuth
stateis HMAC-signed but carries only a randomnoncethat is never recorded or checked server-side, and has no timestamp/TTL. A signed state (and, for native-MCP, the embedded PKCE verifier + DCRclient_id) is therefore valid forever and reusable.Why it matters
Combined with callbacks that only require
session.user.id === state.userId, a leaked callback URL (referer header, logs, browser history) can be replayed by the same user indefinitely.Suggested fix
Add an issued-at timestamp and reject states older than a few minutes. Ideally bind a one-time nonce stored server-side or in a short-lived cookie so each state is single-use.