From 705a93291bcf8ee923c0a9c8d65ecae0bd628cae Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Mon, 18 May 2026 18:32:07 -0700 Subject: [PATCH] fix(roles): pin assignCustomRole to the caller's space (D4) assignCustomRole inserted {member_id, custom_role_id} with no space check on either; the RLS WITH CHECK only validated member_id's space, so an admin of space A could attach space B's custom role to an A member. Now both the custom role and the target member are verified in the caller's space before insert. (Blast radius was bounded by downstream space re-filtering; this closes the scoping defect.) --- lib/actions/roles.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/actions/roles.ts b/lib/actions/roles.ts index cd160bb..95b3aea 100644 --- a/lib/actions/roles.ts +++ b/lib/actions/roles.ts @@ -137,6 +137,26 @@ export async function assignCustomRole(memberId: string, customRoleId: string) { const supabase = await createClient() const auth = await requireMemberWithRole(supabase, ROLE_ADMIN_ROLES, 'Admin or board access required') if (!auth.ok) return { error: auth.error } + const { member } = auth + + // Pin BOTH the target member and the custom role to the caller's space. + // The RLS insert WITH CHECK only validated member_id's space, not the + // custom_role_id's — an admin of space A could otherwise attach space B's + // custom role to a member of A. + const { data: cr } = await supabase + .from('space_custom_roles') + .select('id') + .eq('id', b.data) + .eq('space_id', member.space_id) + .maybeSingle() + if (!cr) return { error: 'Custom role not found in this space.' } + const { data: tm } = await supabase + .from('space_members') + .select('id') + .eq('id', a.data) + .eq('space_id', member.space_id) + .maybeSingle() + if (!tm) return { error: 'Member not found in this space.' } const { error } = await supabase .from('space_member_custom_roles')