Summary
src/app/api/contacts/route.ts POST handler uses getServiceClient() (Supabase service role key) to upsert contacts. This bypasses Row Level Security policies.
Location
// src/app/api/contacts/route.ts line ~57
const serviceClient = getServiceClient();
const { data, error } = await serviceClient
.from("contacts")
.upsert(
{ user_id: user.id, phone, name: name || null },
{ onConflict: "user_id,phone" }
)
Impact
While the user_id is taken from the authenticated session (not user input), the use of serviceClient means:
- Any RLS policies on the
contacts table are completely bypassed
- If the
user.id were ever manipulated upstream, there would be no database-level protection
- This is inconsistent with GET handler which correctly uses the regular Supabase client with RLS
This is the same pattern identified in #24 (phone_numbers) and #30 (providers).
Suggested Fix
Use the regular Supabase client instead of getServiceClient() for the upsert:
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase
.from("contacts")
.upsert(
{ user_id: user.id, phone, name: name || null },
{ onConflict: "user_id,phone" }
)
Severity
Medium — RLS bypass, but user_id comes from authenticated session.
Summary
src/app/api/contacts/route.tsPOST handler usesgetServiceClient()(Supabase service role key) to upsert contacts. This bypasses Row Level Security policies.Location
Impact
While the
user_idis taken from the authenticated session (not user input), the use ofserviceClientmeans:contactstable are completely bypasseduser.idwere ever manipulated upstream, there would be no database-level protectionThis is the same pattern identified in #24 (phone_numbers) and #30 (providers).
Suggested Fix
Use the regular Supabase client instead of
getServiceClient()for the upsert:Severity
Medium — RLS bypass, but user_id comes from authenticated session.