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>
52 lines
1.9 KiB
TypeScript
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.`,
|
|
);
|
|
}
|
|
}
|