From 74ab4dae3daa28c2d3227a13822ba6fd9a8fea5e Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Thu, 28 May 2026 14:13:27 +0000 Subject: [PATCH] submission-receive: defense-in-depth approval check Even though distribute-rmp gates the submitter, re-validate the contributor's distribute.users.status here before inserting into bb.native_articles. A leaked DISTRIBUTE_SHARED_SECRET shouldn't be enough to bypass the approval system. Falls back to letting the request through on transient lookup failure (distribute side already checked). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/submission-receive/route.ts | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/app/api/submission-receive/route.ts b/src/app/api/submission-receive/route.ts index df4dbab..b7c1141 100644 --- a/src/app/api/submission-receive/route.ts +++ b/src/app/api/submission-receive/route.ts @@ -54,6 +54,42 @@ export async function POST(request: NextRequest) { const submissionId = body?.submission_id || null; const property = body?.property || 'broadcastbeat'; + // Defense in depth: re-check the contributor's approval status against + // distribute.users. distribute-rmp's submit route already gates, but a + // belt-and-suspenders check here means even a leaked DISTRIBUTE_SHARED_SECRET + // can't shove articles in from non-approved users. + if (contributor?.id) { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const srKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + if (url && srKey) { + try { + const r = await fetch( + `${url}/rest/v1/users?id=eq.${encodeURIComponent(contributor.id)}&select=status&limit=1`, + { + headers: { + apikey: srKey, + Authorization: `Bearer ${srKey}`, + 'Accept-Profile': 'distribute', + }, + cache: 'no-store', + }, + ); + const rows = (await r.json()) as Array<{ status: string }>; + const status = Array.isArray(rows) && rows.length ? rows[0].status : null; + if (status !== 'approved') { + return NextResponse.json( + { error: 'Contributor not approved', status }, + { status: 403 }, + ); + } + } catch { + // If the lookup fails, fall back to letting the request through + // (distribute-rmp side already gated). Don't break the pipeline on + // a transient supabase glitch. + } + } + } + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!url || !key) return NextResponse.json({ error: 'supabase not configured' }, { status: 500 });