Summary #
- Fixes all 6 High/Medium findings from the OAuth vulnerability audit (1 High login CSRF, 5 Medium), each with pinning tests.
- Anonymous legacy (
off) mode is unchanged except checkpoint file permissions. - Branch is current with
main(includes the paste-callback fallback via a merge).
Changes #
- HIGH — login CSRF: the OAuth state's
appStatemust equalsha256(tx)(the transaction cookie); mismatch fails closed — 403, no token minted. The cookie stays SameSite=Lax by design: the authorization-server → callback hop is a legitimate cross-site top-level GET. - Bounded pending-transaction and login-rate maps: pending tx entries get a 15-min TTL and 5k cap; the rate map is capped at 10k entries and fails open past it (availability over precision — the 20 in-flight cap still bounds concurrent
authorize()work). LEXIDRAW_MCP_TRUST_PROXY(true/false/hop count) wired into the exportedcreateMcpApp(cfg), plus a 120/min global fallback login bucket evaluated after per-IP — a flood one IP gets rejected for cannot drain it.- Bearer tokens carry a sliding 30-day idle TTL (
LEXIDRAW_MCP_TOKEN_TTL_MS;0disables → legacy never-expiring). Expiry is fail-closed (idle or corrupt-timestamp records are deleted);lastUsedAtrefresh is throttled to one write per 10 min so the auth path stays read-only in the common case./whoaminow reports{label, createdAt, lastUsedAt, expiresAt}. - Legacy
FileCheckpointStore: 0700 dir / 0600 files via atomic tmp+rename; anonymous-mode behavior otherwise unchanged. - Loopback HTTP binds
127.0.0.1by default;LEXIDRAW_MCP_BIND_HOSToverrides; hosted mode keeps all-interfaces (the reverse proxy is the front). - Docs:
CLAUDE.mdrenamed to a lean top-levelAGENTS.md, with the Lexidraw domain contracts and a consolidatedLEXIDRAW_MCP_*env table in a newsrc/atproto/AGENTS.md. The paste branch's docs-assertion test was dropped at merge (tests pin behavior, not prose).
Validation #
npx vitest run→ 182/182 passing (merged suite: security + paste-flow tests).npm run build→ clean (tsc → vite singlefile → server tsc → bun bundles).npx tsc --noEmit→ clean;git log main..HEAD -- README.md→ empty.
Review notes #
- A rejected cross-transaction callback still consumes the single-use state and stores a session before the 403 — unavoidable without forking
@atproto/oauth-client, and harmless (no token is minted; sessions are DID-keyed, not cookie-keyed). - Deploy notes: in-flight logins from a pre-upgrade server fail once (old states carry no binding — restart the login); idle CLI tokens now expire after 30 days unless
LEXIDRAW_MCP_TOKEN_TTL_MS=0. - The 10k fail-open rate-map cap is a deliberate, documented availability tradeoff.