Settlement loop has no per-payment error isolation; a malformed zap request drops webhooks and zaps for other paylinks #1

Open
opened 2026-10-09 17:12:06 +00:00 by padreug · 0 comments
Owner

wait_for_paid_invoices (tasks.py:20-22) awaits on_invoice_paid inside while True with no try/except. send_zap (tasks.py:111-139) does json.loads(nostr) and assert pubkey on data that comes straight from the public LNURL callback's ?nostr= query parameter (views_lnurl.py:97-99, stored verbatim). Any payer of any zaps-enabled link can therefore make the shared listener raise after their invoice settles. lnbits core restarts the task after 5 s (catch_everything_and_restart), but the restart re-registers a fresh invoice queue, so the triggering payment's webhook and any payments queued behind it are lost — including plain (non-zap) webhook deliveries for unrelated tenants' links. The attacker controls timing and can repeat at will for the cost of a minimum-amount payment.

Fix direction: wrap the per-payment body in wait_for_paid_invoices in try/except (log and continue); make send_zap defensive (guard json.loads, early-return instead of assert); ideally reject malformed zap requests at the callback before creating the invoice (see sandbox lnurlp#3). Regression test: enqueue a payment with extra["nostr"] = "not json" followed by a valid one and assert the second still fires its webhook.

Found during reforge run #1 (sandbox lnurlp#4).

`wait_for_paid_invoices` (`tasks.py:20-22`) awaits `on_invoice_paid` inside `while True` with no try/except. `send_zap` (`tasks.py:111-139`) does `json.loads(nostr)` and `assert pubkey` on data that comes straight from the public LNURL callback's `?nostr=` query parameter (`views_lnurl.py:97-99`, stored verbatim). Any payer of any zaps-enabled link can therefore make the shared listener raise after their invoice settles. lnbits core restarts the task after 5 s (`catch_everything_and_restart`), but the restart re-registers a fresh invoice queue, so the triggering payment's webhook and any payments queued behind it are lost — including plain (non-zap) webhook deliveries for unrelated tenants' links. The attacker controls timing and can repeat at will for the cost of a minimum-amount payment. Fix direction: wrap the per-payment body in `wait_for_paid_invoices` in try/except (log and continue); make `send_zap` defensive (guard `json.loads`, early-return instead of `assert`); ideally reject malformed zap requests at the callback before creating the invoice (see sandbox lnurlp#3). Regression test: enqueue a payment with `extra["nostr"] = "not json"` followed by a valid one and assert the second still fires its webhook. Found during reforge run #1 (sandbox lnurlp#4).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/lnurlp#1
No description provided.