From 94021e2e4f5ed33a897438b805fba56f71485577 Mon Sep 17 00:00:00 2001 From: FuturMix Date: Sun, 14 Jun 2026 12:18:15 +0800 Subject: [PATCH] fix: prevent IDOR in store invoice endpoint The invoice GET endpoint queries by ID without filtering by user_id, then checks ownership after retrieval. This leaks invoice existence (404 vs 403 status codes) and relies on application-level checks instead of query-level filtering. Move the user_id filter into the query itself, and replace the raw error.message with a generic error to prevent information disclosure. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/app/api/store/invoices/[id]/route.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/api/store/invoices/[id]/route.ts b/apps/web/src/app/api/store/invoices/[id]/route.ts index b5957f9..c9fb4eb 100644 --- a/apps/web/src/app/api/store/invoices/[id]/route.ts +++ b/apps/web/src/app/api/store/invoices/[id]/route.ts @@ -38,6 +38,9 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string from: (t: string) => { select: (s: string) => { eq: (col: string, val: string) => { + eq: (col: string, val: string) => { + maybeSingle: () => Promise<{ data: InvoiceRow | null; error: { message: string } | null }>; + }; maybeSingle: () => Promise<{ data: InvoiceRow | null; error: { message: string } | null }>; }; }; @@ -46,10 +49,10 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string const { data, error } = await c.from('plugin_invoices') .select('id, user_id, plugin_id, coinpay_payment_id, amount_usd, blockchain, payment_address, crypto_amount, qr_code, status, expires_at, paid_at, forwarded_at, created_at, updated_at') .eq('id', id) + .eq('user_id', auth.userId) .maybeSingle(); - if (error) return Response.json({ error: error.message }, { status: 500 }); + if (error) return Response.json({ error: 'Failed to fetch invoice' }, { status: 500 }); if (!data) return Response.json({ error: 'invoice not found' }, { status: 404 }); - if (data.user_id !== auth.userId) return Response.json({ error: 'unauthorized' }, { status: 403 }); // Optionally refresh status from Coinpay so the UI shows fast progress // without waiting for the webhook to land.