diff options
| author | Adam Soltys <[email protected]> | 2026-09-29 08:37:39 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-29 08:37:47 -0700 |
| commit | 41a5819f5d7c662a5a2884952e49a14464e83c41 (patch) | |
| tree | 54b690578204c920269d31470d694984a80f776d | |
| parent | 598a142e852b4682888aceac0d2d8dc39e0f981c (diff) | |
| download | coinos-server-41a5819f5d7c662a5a2884952e49a14464e83c41.tar.gz coinos-server-41a5819f5d7c662a5a2884952e49a14464e83c41.zip | |
payments: refuse on-chain sends to blocked addresses before debiting
debit() checks blocked_addresses against the payment hash, which for an
on-chain send is the txid, so a blocked destination address was never
matched. Check each output address of the built transaction in sendOnchain
and fail before any debit.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
| -rw-r--r-- | lib/payments.ts | 19 |
1 files changed, 19 insertions, 0 deletions
diff --git a/lib/payments.ts b/lib/payments.ts index f2a05d50..84d2f25a 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -709,6 +709,25 @@ export const sendOnchain = async (params) => { const r = await node.testMempoolAccept([hex]); if (!r[0].allowed) fail("transaction rejected"); + // Known drain destinations. debit() checks `blocked_addresses` against + // its hash, which for an on-chain send is the txid, so the address itself + // was never matched here. Refuse before the debit: a drain operator can + // fund fresh throwaway accounts and sweep each one on-chain to the same + // address, so freezing accounts is a treadmill while the destination is + // constant. + for (const { + scriptPubKey: { address }, + } of tx.vout) { + if (address && (await db.sIsMember("blocked_addresses", address))) { + const n = quiet(`blockedaddr:${user.id}`, 5 * 60 * 1000); + if (n !== null) + err( + `SECURITY: blocked on-chain send to ${address} by ${user.username} ${suppressed(n)}`, + ); + fail("address blocked"); + } + } + let total = 0; let fee = 0; let change = 0; |
