Skip to content

fix(wallets): stop getUserWallets listing anyone's wallets without a session - #433

Open
QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-11-02-user-wallets-auth
Open

QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-11-02-user-wallets-auth

Conversation

@QSchlegel

@QSchlegel QSchlegel commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

What

wallet.getUserWallets (tRPC, publicProcedure) only checked the requested address when the caller had a session. With no session at all it ran the query anyway, so

GET /api/trpc/wallet.getUserWallets?input={"json":{"address":"addr1…"}}

returned, to anyone, every wallet row that names that address as a signer — name, description, signersDescriptions (people's labels), rawImportBodies, script CBOR. Signer addresses are public on-chain, so the address proves nothing. getWallet already refuses anonymous callers; the list exposed more than the single-wallet read.

Change

  • Server (src/server/api/routers/wallets.ts): no session → UNAUTHORIZED; session that doesn't hold the address → FORBIDDEN (unchanged). Same shape as getWallet.
  • Client (src/hooks/useUserWallets.ts): the hook relied on the anonymous read to show the list before the user signed the session nonce. It now waits for authorization and returns needsAuthorization (also true when the list call comes back 401, e.g. expired cookie). A failed session check is not treated as "unauthorized" — the modal only opens on an answer.
  • /wallets (homepage/wallets/index.tsx): shows an "Authorize wallet" prompt (requestReauth → reopens the auth modal) instead of the "Get started" empty state when the user is connected but not authorized. getUserNewWallets / getUserNewWalletsNotOwner are protectedProcedures that 401'd and retried without a session; they're now gated on isAuthorized and 401 is not retried.

Unaffected callers (checked): /api/v1/walletIds and the MCP walletIds tool build their own caller with the JWT address as session; CI scripts and e2e use bot JWTs or an injected mesh_wallet_session cookie.

Tests

  • New src/__tests__/trpc/userWalletsAuth.test.ts (6): anonymous → 401 with no DB read, wrong address (wallet session and NextAuth) → 403, own address / multi-wallet session / NextAuth user → list. Anonymous case fails on preprod (resolves with the wallet row), passes here.
  • npm run test:trpc against local Postgres: 15 suites, 117 tests passed.
  • node scripts/run-tests.mjs: 1656 passed, 2 skipped (+ 97 in the second group), exit 0.
  • npx tsc --noEmit: clean.
  • next build --webpack + bundle budget: within budget; /wallets 1920.7 → 1920.9 KB gz.
  • Independent cold review (two passes); its findings on the client UX (empty state for unauthorized users, dead button on a failed session check, expired cookie) are fixed here.

Not verified / out of scope

  • No browser run of the authorize prompt; no hook-level test (the repo has no React testing library). Flow traced in code: requestReauth → layout clears its latch → refetch → modal opens.
  • Needs a decision (Quirin): GET /api/v1/exportWallet/listMine is CORS-public and returns id, name, description, type, numRequiredSigners, numSigners for any address/stake key by design ("no proof of ownership is required") — same leak class, but the cross-instance import flow depends on it, so closing it needs a nonce-sign step. user.getUserByAddress is handled in fix(users): stop the public user lookup handing out linked Discord IDs #420.

https://claude.ai/code/session_017rkSXEzcv4V5UecnbAuNvd


Generated by Claude Code

…session

wallet.getUserWallets skipped its address check when the caller had no
session at all, so an anonymous request for any signer address (public
on-chain) returned the full wallet rows that name it: names, descriptions,
signer labels and import bodies. getWallet already refuses anonymous
callers; the list now does too (401 without a session, 403 for an address
the session does not hold).

The client relied on the anonymous read to show the list before the user
signed the session nonce. useUserWallets now waits for authorization and
reports needsAuthorization, and /wallets shows an "Authorize wallet" prompt
(reopening the auth modal) instead of claiming the user has no wallets.
The two wallet-creation lists on that page are protectedProcedures and are
gated the same way, with 401 no longer retried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rkSXEzcv4V5UecnbAuNvd
@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
multisig Ready Ready Preview Nov 2, 2026 6:20am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 70f7d981 Deployed Nov 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants