From edf7e6863a4c71635d09dba540b0c0a9a8e29d26 Mon Sep 17 00:00:00 2001 From: chenlinxi890-spec Date: Wed, 8 Jul 2026 14:45:21 +0800 Subject: [PATCH] fix(bounty): prevent cross-store payout and coupon reuse in claim flow (#37) Add two critical validations to POST /api/bounties/[id]/claim: 1. Validate coupon store matches bounty store (prevents cross-store exploitation) 2. Prevent coupon reuse (a coupon can only claim one bounty) This fixes a security vulnerability where any user could claim any bounty with any coupon, potentially draining the merchant wallet. --- apps/web/app/api/bounties/[id]/claim/route.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/web/app/api/bounties/[id]/claim/route.ts b/apps/web/app/api/bounties/[id]/claim/route.ts index 7d5c0e2..a3dc04b 100644 --- a/apps/web/app/api/bounties/[id]/claim/route.ts +++ b/apps/web/app/api/bounties/[id]/claim/route.ts @@ -30,6 +30,24 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const coupons = await db.sql`SELECT * FROM coupons WHERE id = ${coupon_id}`; if (!coupons.length) return NextResponse.json({ error: 'Coupon not found' }, { status: 404 }); + // Validate coupon store matches bounty store (prevents cross-store payout exploitation) + const coupon = coupons[0]; + if (bounty.store_id && coupon.store_id !== bounty.store_id) { + return NextResponse.json( + { error: 'Coupon store does not match bounty store' }, + { status: 400 } + ); + } + + // Prevent coupon reuse — a coupon can only claim one bounty + const reused = await db.sql`SELECT id FROM bounties WHERE coupon_id = ${coupon_id} AND status IN ('claimed', 'paid')`; + if (reused.length) { + return NextResponse.json( + { error: 'This coupon has already been used to claim another bounty' }, + { status: 409 } + ); + } + // Mark bounty as claimed — atomic WHERE prevents race conditions await db.sql` UPDATE bounties