diff options
| author | Adam Soltys <[email protected]> | 2026-08-07 23:00:07 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-07 23:29:26 -0700 |
| commit | c07dfe050b7c79917744d7eddbf934d319cb9c89 (patch) | |
| tree | b4da142be1a70b62b4ba29f5f45b6626358d870f /lib | |
| parent | a95a0dfd27980b01dbfe723d8be1ca4535b7d96a (diff) | |
| download | coinos-server-c07dfe050b7c79917744d7eddbf934d319cb9c89.tar.gz coinos-server-c07dfe050b7c79917744d7eddbf934d319cb9c89.zip | |
payments: never lose a settled deposit to an oversized memo (S9/S10)
credit() ran fail("memo too long") on memos >5000 chars, but it runs
AFTER settlement: an incoming lightning/bolt12 deposit has already
advanced pay_index, and an internal send has already debited the sender.
The throw was only logged, replay() hit the same throw, and the sats sat
in the house wallet uncredited. The memo is attacker-controllable and was
uncapped via the lnurl comment param and the bolt12 payer_note.
Truncate the memo in credit() instead of throwing (a clipped note is
harmless), and cap the lnurl comment to the advertised 512 at ingress.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/payments.ts | 9 |
1 files changed, 8 insertions, 1 deletions
diff --git a/lib/payments.ts b/lib/payments.ts index 2fb891a2..960ff705 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -354,7 +354,14 @@ export const credit = async ({ tip = tip !== undefined ? Number.parseInt(tip) || 0 : Number.parseInt(inv.tip) || 0; if (!memo) ({ memo } = inv); - if (memo && memo.length > 5000) fail("memo too long"); + // Truncate rather than throw: credit() runs AFTER the payment has settled (an + // incoming lightning/bolt12 deposit already advanced pay_index; an internal + // send already debited the sender). Throwing here left the sats in the house + // wallet with no retry path — the payer saw a completed payment and the + // recipient was never credited. An oversized memo (attacker-controllable via + // the lnurl comment or a bolt12 payer_note) must never destroy a settled + // payment; a clipped note is harmless. + if (memo && memo.length > 5000) memo = memo.slice(0, 5000); if (amount < 0 || tip < 0) fail("Invalid amount"); // For a NON-internal receipt the tip is only a cosmetic split of the real // settled `amount` — there is no paired coinos debit carrying it — so it can |
