Skip to content

fix(cashout): enforce caller wallet ownership on cashout mutations - #465

Open
islandbitcoin wants to merge 4 commits into
mainfrom
fix/cashout-resolver-authz
Open

fix(cashout): enforce caller wallet ownership on cashout mutations#465
islandbitcoin wants to merge 4 commits into
mainfrom
fix/cashout-resolver-authz

Conversation

@islandbitcoin

Copy link
Copy Markdown
Contributor

Summary

  • Security fix (IDOR / missing object-level authz): requestCashOut and initiateCashOut enforced only "any valid session" — the resolvers never read domainAccount, so any authenticated user could create and execute cashout offers against any walletId, forcing liquidation of another account's USD/USDT balance. Payout stayed bound to the victim's own ERPNext bank accounts (verifyBankAccount), so this was unauthorized liquidation / griefing rather than direct theft — but fully unauthorized either way.
  • Ownership is now enforced at the shared seam: CashoutManager.createOffer / executeCashout require the caller's accountId and reject foreign wallets before any Ibex/ERPNext side effect. Both resolvers pass domainAccount.id, matching the existing lnurl-payment-send pattern; the prior provided-vs-settlement wallet check remains as defense in depth.
  • New unit spec cashout-manager-authz.spec.ts proves the cross-account rejection on both entry points (written red-first) and that legitimate flows still proceed. Existing offers suites updated to the new signatures.

Test plan

  • npx jest --config ./test/flash/unit/jest.config.js offers — 16/16 pass (4 suites)
  • tsc --noEmit -p tsconfig.d.json && tsc --noEmit — clean
  • npx eslint on changed files — clean
  • Integration specs (test/flash/integration/offers/*) updated to the new signatures; require the docker dev stack to run

requestCashOut and initiateCashOut never read the auth context: any
authenticated user could create and execute cashout offers against any
walletId, forcing liquidation of another account's USD/USDT balance.
Payout stayed bound to the victim's own bank accounts, so the impact
was unauthorized liquidation rather than direct theft, but the action
was fully unauthorized either way.

CashoutManager now takes the caller's accountId and rejects foreign
wallets before any Ibex/ERPNext side effect; both resolvers pass
domainAccount.id, matching the existing lnurl-payment-send pattern.
New unit spec covers the cross-account rejection on both entry points.
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