Security fix: prevent AuthToken UTxOs from leaking to proxyAddress as unguarded change - #396
Open
Olisehgenesis wants to merge 1 commit into
Open
Olisehgenesis wants to merge 1 commit into
Olisehgenesis wants to merge 1 commit into
Conversation
…ted change buildProxySpendTx (and buildProxyCleanupSweepTx) only excluded the single designated authTokenUtxo from caller-supplied walletUtxos before spending them and sending changeAddress(proxyAddress). A wallet holds up to 10 AuthToken UTxOs (minted at proxy setup, all sent to the wallet's own address). If any *other* AuthToken UTxO was included in walletUtxos - e.g. by a bot/integration doing ordinary coin selection via freeUtxos, which returned all UTxOs undifferentiated - it would silently become change at proxyAddress. The on-chain proxy spend validator only checks that an AuthToken appears in *some* output, with no signer/quorum check, so once an AuthToken sits at proxyAddress unprotected, anyone can drain the address with a raw transaction - no compromised keys required. Changes: - utxoUtils: add assertNoStrayAuthTokenUtxos(), a fail-closed guard that throws if walletUtxos contains any AuthToken-bearing UTxO other than the one explicitly designated. - txBuilders: call the guard in buildProxySpendTx and buildProxyCleanupSweepTx before building the transaction. - api/v1/freeUtxos: flag each returned UTxO with authToken: true/false (based on the wallet's active proxy AuthToken policy id) so callers have a positive signal to avoid selecting more than the one they intend. - SKILL.md: document the authToken flag and the exactly-one-AuthToken-UTxO requirement for proxySpend callers. - tests: new proxyAuthTokenLeakGuard.test.ts covering the guard and buildProxySpendTx rejection; updated freeUtxos.bot.test.ts mocks/assertions for the new authToken field. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
@Olisehgenesis is attempting to deploy a commit to the MeshJS Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability
buildProxySpendTx(andbuildProxyCleanupSweepTx) only excluded the single designatedauthTokenUtxofrom caller-suppliedwalletUtxosbefore spending them, withchangeAddress(proxyAddress).A proxy-enabled wallet holds up to 10 AuthToken UTxOs (all 10 minted at proxy setup, sent to the wallet's own regular address as a pool of "spend permission" tokens - see
buildProxySetupTx).GET /api/v1/freeUtxosreturns all wallet UTxOs undifferentiated, with no signal distinguishing a plain ADA UTxO from an AuthToken UTxO.If a caller (a bot/automation integration doing ordinary coin selection for fees/funding) supplied
utxoRefsto/api/v1/proxySpendthat happened to include any of the other 9 AuthToken UTxOs alongside the one intended for the call, that extra AuthToken would be spent as a normal input and never explicitly re-output - so it fell through tochangeAddress(proxyAddress)as unprotected change.The on-chain proxy spend validator (
validators/proxy/spend.ak) only checks that an AuthToken appears in some output of the spending transaction - it has no signer/quorum check. Once an AuthToken sits atproxyAddressthis way, anyone can construct a raw transaction spending everything there, satisfying the validator by sending the AuthToken to any output (e.g. their own address), and keep the rest. No compromised signer keys or wallet credentials are required for that final step - only the initial (non-malicious) inclusion of a stray AuthToken UTxO in a proxy spend's funding inputs.Fix
src/lib/proxy/utxoUtils.ts: newassertNoStrayAuthTokenUtxos()- fails closed (throws) ifwalletUtxoscontains any AuthToken-bearing UTxO other than the one explicitly designated for the transaction.src/lib/proxy/txBuilders.ts: call the guard at the top ofbuildProxySpendTxandbuildProxyCleanupSweepTx, before any inputs are added - so an unsafe call is rejected outright instead of silently leaking funds.src/pages/api/v1/freeUtxos.ts: each returned UTxO now carries anauthToken: booleanfield (true if it carries any of the wallet's active proxy AuthToken policy ids), giving callers a positive signal during coin selection..cursor/skills/multisig/SKILL.md: documented theauthTokenflag and the "exactly one AuthToken UTxO per proxySpend call" requirement for bot integrations.Testing
src/__tests__/proxyAuthTokenLeakGuard.test.ts: unit tests forassertNoStrayAuthTokenUtxos, plusbuildProxySpendTxregression tests proving it now throws when a stray AuthToken UTxO is included, and still builds normally for legitimate plain-ADA funding.src/__tests__/freeUtxos.bot.test.tsmocks/assertions for the newauthTokenfield.node scripts/run-tests.mjs→ 1032+87 tests passing, 0 failures.tsc --noEmitclean.Scope note
This is an API/tx-builder-layer, defense-in-depth fix for the currently deployed, immutable on-chain validator (which cannot itself be patched retroactively for already-deployed proxy addresses). It closes the practical leak path without requiring a validator upgrade or migration.
I verified, read-only via Blockfrost, that this exact contract pattern is live on mainnet with real funds at stake (a script-credentialed DRep/proxy wallet holding ~1,530 ADA, plus two others), and confirmed via direct source review that this is the mechanism by which a stray AuthToken UTxO reaches
proxyAddressunprotected. Happy to share those findings privately if useful for triage.