Skip to content

Security fix: prevent AuthToken UTxOs from leaking to proxyAddress as unguarded change - #396

Open
Olisehgenesis wants to merge 1 commit into
MeshJS:mainfrom
Olisehgenesis:fix/proxy-authtoken-leak-guard
Open

Olisehgenesis wants to merge 1 commit into
MeshJS:mainfrom
Olisehgenesis:fix/proxy-authtoken-leak-guard

Conversation

@Olisehgenesis

Copy link
Copy Markdown

Vulnerability

buildProxySpendTx (and buildProxyCleanupSweepTx) only excluded the single designated authTokenUtxo from caller-supplied walletUtxos before spending them, with changeAddress(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/freeUtxos returns 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 utxoRefs to /api/v1/proxySpend that 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 to changeAddress(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 at proxyAddress this 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: new assertNoStrayAuthTokenUtxos() - fails closed (throws) if walletUtxos contains any AuthToken-bearing UTxO other than the one explicitly designated for the transaction.
  • src/lib/proxy/txBuilders.ts: call the guard at the top of buildProxySpendTx and buildProxyCleanupSweepTx, 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 an authToken: boolean field (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 the authToken flag and the "exactly one AuthToken UTxO per proxySpend call" requirement for bot integrations.

Testing

  • New src/__tests__/proxyAuthTokenLeakGuard.test.ts: unit tests for assertNoStrayAuthTokenUtxos, plus buildProxySpendTx regression tests proving it now throws when a stray AuthToken UTxO is included, and still builds normally for legitimate plain-ADA funding.
  • Updated src/__tests__/freeUtxos.bot.test.ts mocks/assertions for the new authToken field.
  • Full suite: node scripts/run-tests.mjs → 1032+87 tests passing, 0 failures.
  • tsc --noEmit clean.

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 proxyAddress unprotected. Happy to share those findings privately if useful for triage.

…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>
@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown

@Olisehgenesis is attempting to deploy a commit to the MeshJS Team on Vercel.

A member of the Team first needs to authorize it.

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.

1 participant