Harden auth, payments, and frontend against review findings.
Close exploitable gaps in booking/payment flows, enforce token versioning and account checks, gate sensitive payment data, and add middleware plus input validation across admin routes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { streamSSE } from 'hono/streaming';
|
||||
import { db, dbGet, dbAll, tickets, payments } from '../db/index.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { getNow } from '../lib/utils.js';
|
||||
import { verifyWebhookPayment, getPaymentStatus } from '../lib/lnbits.js';
|
||||
import emailService from '../lib/email.js';
|
||||
@@ -111,13 +111,23 @@ function stopBackgroundChecker(ticketId: string) {
|
||||
*/
|
||||
lnbitsRouter.post('/webhook', async (c) => {
|
||||
try {
|
||||
// Optional shared-secret gate: if LNBITS_WEBHOOK_SECRET is configured, the
|
||||
// webhook URL must carry a matching ?token=... (set when the invoice is created).
|
||||
const webhookSecret = process.env.LNBITS_WEBHOOK_SECRET || '';
|
||||
if (webhookSecret) {
|
||||
const provided = c.req.query('token') || c.req.header('x-webhook-secret') || '';
|
||||
if (provided !== webhookSecret) {
|
||||
console.warn('LNbits webhook rejected: invalid or missing secret');
|
||||
return c.json({ received: true, processed: false }, 401);
|
||||
}
|
||||
}
|
||||
|
||||
const payload: LNbitsWebhookPayload = await c.req.json();
|
||||
|
||||
// Log identifiers only (no full payload / PII)
|
||||
console.log('LNbits webhook received:', {
|
||||
paymentHash: payload.payment_hash,
|
||||
status: payload.status,
|
||||
amount: payload.amount,
|
||||
extra: payload.extra,
|
||||
});
|
||||
|
||||
// Verify the payment is actually complete by checking with LNbits
|
||||
@@ -135,6 +145,20 @@ lnbitsRouter.post('/webhook', async (c) => {
|
||||
return c.json({ received: true, processed: false }, 200);
|
||||
}
|
||||
|
||||
// CRITICAL: bind the paid hash to this ticket's own invoice. Without this, a
|
||||
// valid paid hash from any other invoice could be replayed with an arbitrary
|
||||
// ticketId to confirm tickets for free.
|
||||
const ticketPayment = await dbGet<any>(
|
||||
(db as any).select().from(payments).where(eq((payments as any).ticketId, ticketId))
|
||||
);
|
||||
if (!ticketPayment || ticketPayment.reference !== payload.payment_hash) {
|
||||
console.warn('LNbits webhook rejected: payment hash does not match the ticket invoice', {
|
||||
ticketId,
|
||||
paymentHash: payload.payment_hash,
|
||||
});
|
||||
return c.json({ received: true, processed: false }, 200);
|
||||
}
|
||||
|
||||
// Stop background checker since webhook confirmed payment
|
||||
stopBackgroundChecker(ticketId);
|
||||
|
||||
@@ -186,15 +210,13 @@ async function handlePaymentComplete(ticketId: string, paymentHash: string) {
|
||||
console.log(`Multi-ticket booking detected: ${ticketsToConfirm.length} tickets to confirm`);
|
||||
}
|
||||
|
||||
// Confirm all tickets in the booking
|
||||
// Confirm all tickets in the booking (idempotent: only flip pending -> confirmed)
|
||||
for (const ticket of ticketsToConfirm) {
|
||||
// Update ticket status to confirmed
|
||||
await (db as any)
|
||||
.update(tickets)
|
||||
.set({ status: 'confirmed' })
|
||||
.where(eq((tickets as any).id, ticket.id));
|
||||
.where(and(eq((tickets as any).id, ticket.id), eq((tickets as any).status, 'pending')));
|
||||
|
||||
// Update payment status to paid
|
||||
await (db as any)
|
||||
.update(payments)
|
||||
.set({
|
||||
@@ -203,7 +225,7 @@ async function handlePaymentComplete(ticketId: string, paymentHash: string) {
|
||||
paidAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq((payments as any).ticketId, ticket.id));
|
||||
.where(and(eq((payments as any).ticketId, ticket.id), eq((payments as any).status, 'pending')));
|
||||
|
||||
console.log(`Ticket ${ticket.id} confirmed via Lightning payment (hash: ${paymentHash})`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user