SkateSpots/src/lib/verifyInvoice.ts
shakespeare.diy be05a603be security: verify invoice amounts + require provider-issued zap receipts (1.2.14)
Fixes the audit findings:

HIGH-1 (NWC/WebLN auto-pay of unverified invoices): add assertInvoiceAmount and
call it before every programmatic payment in useZaps, useCrewZap, and
SpamFeePayment. A malicious LNURL/Lightning-address server can no longer return
an inflated invoice and drain the wallet — amount is decoded from bolt11 and must
match the intended sats (±1 sat), amountless invoices are rejected.

HIGH-2 (forgeable trust signals):
- useSasquatchPro: Pro is granted ONLY by a zap receipt AUTHORED by the
  platform's Lightning provider, embedding a user-signed zap request that tags
  the platform, with a real paid amount >= smallest tier. The self-signed
  kind:32671 receipt is now advisory only and never grants access (closed the
  "self-grant permanent Pro" hole).
- useFeaturedVerification: a listing is 'verified' only when the matching 9735 is
  authored by the provider pubkey (not any self-published receipt).
- useCrewGoal: tally only invoice-backed (bolt11) zaps; the forgeable amount tag
  is no longer summed, so goal progress can't be inflated with fake receipts.

Bump to 1.2.14.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 11:45:21 +07:00

52 lines
1.9 KiB
TypeScript

/**
* Invoice amount verification for auto-pay flows.
*
* SECURITY (HIGH): Any invoice returned by a third-party LNURL / Lightning
* address server MUST be verified before it is paid programmatically (NWC /
* WebLN). Otherwise a malicious zap target can return a 1,000,000-sat invoice
* for a 21-sat zap and the app pays it silently. We decode the bolt11 amount
* ourselves and refuse to auto-pay if it doesn't match what the user intended.
*/
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
/**
* Assert that a bolt11 invoice encodes an amount matching `expectedSats`.
*
* - Rejects a malformed / unparseable invoice.
* - Rejects an amountless invoice (0) when a concrete amount was expected — an
* amountless invoice would let the wallet be prompted for an arbitrary amount.
* - Allows a tiny tolerance for rounding between millisat/sat conversions.
*
* @throws Error with a clear message if the invoice must NOT be auto-paid.
*/
export function assertInvoiceAmount(invoice: string, expectedSats: number): void {
if (!invoice || typeof invoice !== 'string') {
throw new Error('The Lightning service returned an invalid invoice.');
}
let invoiceSats: number;
try {
invoiceSats = getSatoshisFromBolt11(invoice);
} catch {
throw new Error(
'Could not verify the invoice amount — refusing to auto-pay for your safety.',
);
}
// Amountless invoice: never auto-pay when we intended a specific amount.
if (invoiceSats === 0 && expectedSats > 0) {
throw new Error(
'The invoice has no fixed amount — refusing to auto-pay for your safety.',
);
}
// Allow 1 sat of rounding slack (millisat↔sat conversions).
const diff = Math.abs(invoiceSats - expectedSats);
if (diff > 1) {
throw new Error(
`Invoice amount mismatch: expected ${expectedSats} sats but the invoice ` +
`requests ${invoiceSats} sats. Payment blocked for your safety.`,
);
}
}