Skip to main content

Self-provisioning privilege escalation

Two permissive RLS policies combine into an escalation path:

  • companies INSERT WITH CHECK (true) — any authenticated user can create a company.
  • "Users can create own membership" WITH CHECK (user_id = auth.uid()) — a user can insert a company_memberships row 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.