aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-29 08:37:39 -0700
committerAdam Soltys <[email protected]>2026-09-29 08:37:47 -0700
commit41a5819f5d7c662a5a2884952e49a14464e83c41 (patch)
tree54b690578204c920269d31470d694984a80f776d /lib
parent598a142e852b4682888aceac0d2d8dc39e0f981c (diff)
downloadcoinos-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]>
Diffstat (limited to 'lib')
-rw-r--r--lib/payments.ts19
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;