Skip to content

fix(migrations): only a wallet's signers may open or read its migration - #441

Open
QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-11-12-migration-wallet-auth
Open

QSchlegel wants to merge 1 commit into
preprodfrom
claude/daily-2026-11-12-migration-wallet-auth

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

What

Two migration tRPC procedures did not check that the caller belongs to the wallet being migrated:

  • createMigration checked that the session controls ownerAddress, but not that ownerAddress belongs to originalWalletId. A wallet may have only one active migration (pending/in_progress), so any signed-in user could open one for someone else's wallet id. The real signers were then locked out of migrating, and only the stranger could cancel. The CONFLICT message also told a stranger who holds an existing migration.
  • getMigrationByOriginalWallet returned the active migration (initiator address, migrationData) to any session at all.

Change

  • createMigration: the initiating address itself must be a signer or the owner of the original wallet. This is checked inline rather than with assertWalletAccess, because that helper also accepts any other address in a multi-wallet session, and the lock would then be recorded under a non-signer address.
  • getMigrationByOriginalWallet: assertWalletAccess on the wallet. The query stays wallet-scoped, not owner-scoped, so co-signers still see a migration another signer started (the "already in progress" notice in migrate-wallet.tsx).

Legitimate flows are unchanged. The migrate page only renders after wallet.getWallet, which already requires the session address to be a signer.

Tests

  • New src/__tests__/trpc/migrationAuth.test.ts (10 tests, mock DB): stranger refused and no lock created; existing initiator not leaked; unknown wallet → NOT_FOUND; signer can create; mismatched ownerAddress refused before any wallet lookup; multi-wallet session can't record a non-signer initiator; anonymous / stranger reads refused before the migration is read; co-signer sees another signer's migration; null when none is active.
  • Before the fix, 4 of the original 8 tests fail. After it, all pass.
  • npx tsc --noEmit ✅
  • tRPC suite against local Postgres (jest -c jest.trpc.config.mjs --runInBand): 17 suites / 138 tests passed
  • Full unit run node scripts/run-tests.mjs: CJS 185 suites / 2352 passed (2 skipped), ESM 13 / 140 passed
  • next build --webpack + check-bundle-budget --report: within budget. This is a server-only change, so bundles are unchanged.
  • An independent review agent read the diff cold. Its multi-wallet finding is fixed in this PR.

Not covered / follow-ups (need a decision)

  • Existing squatted locks: rows created before this fix by non-signers can only be cancelled by their creator. To check:
    SELECT m.id FROM "Migration" m JOIN "Wallet" w ON w.id = m."originalWalletId"
    WHERE m.status IN ('pending','in_progress')
      AND m."ownerAddress" <> w."ownerAddress"
      AND NOT (m."ownerAddress" = ANY (w."signersAddresses"));
    Not run: no production access.
  • updateMigrationStep / updateMigrationData / cancelMigration / completeMigration / getMigration check only migration.ownerAddress. They do not re-check wallet membership (for example, an initiator later removed as a signer). updateMigrationStep also accepts any newWalletId unchecked. Both are left out to keep this PR small.
  • The one-active-migration rule is findFirst + create with no unique constraint, so it is racy. Fixing it needs a schema migration, so it is out of scope here.

Generated by Claude Code

createMigration checked that the caller controls ownerAddress, but never that
ownerAddress belongs to the wallet being migrated. Since a wallet may have only
one active migration, any signed-in user could open one for someone else's
wallet id and lock its real signers out of migrating until they cancelled it;
the CONFLICT message also told them who holds an existing migration.
getMigrationByOriginalWallet returned the active migration (initiator address,
migration plan) to any session at all.

- createMigration: the initiating address itself must be a signer or the owner
  of the original wallet (not just any address in a multi-wallet session).
- getMigrationByOriginalWallet: caller must have access to the wallet; it stays
  wallet-scoped, not owner-scoped, so co-signers still see a migration another
  signer started.

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

vercel Bot commented Nov 12, 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 12, 2026 6:07am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — b2816916 Deployed Nov 12, 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