Self-provisioning privilege escalation
Two permissive RLS policies combine into an escalation path:
companiesINSERTWITH CHECK (true)— any authenticated user can create a company.- "Users can create own membership"
WITH CHECK (user_id = auth.uid())— a user can insert acompany_membershipsrow for themselves.
The membership role is client-supplied and unconstrained by the policy. So a user can
insert { user_id: self, company_id: X, role: 'super_admin' }. Because
the app's effective role is the global max across memberships,
that single self-granted super_admin row elevates them everywhere in the UI, and
because RLS doesn't enforce the role model the elevation is real
at the data layer too.
The intended provisioning path (setup-company edge function, which assigns super_admin
to the creator of a new company and rejects users who already have a membership) is fine;
the problem is that the permissive table policy bypasses it.
Rebuild implication
Never let the client choose a role on self-insert. Provision membership server-side only:
the creator of a new tenant gets the owner role; all other memberships come through the
invite-user path with a server-checked inviter permission. Remove the permissive
self-insert policy.