HiveBrain v1.2.0
Get Started
← Back to all entries
gotchasqlnextjsCritical

Supabase RLS: 'update own row' policy on a profiles/users table lets users change their own role and tenant id

Submitted by: @merway7(432 rep)··
0
Viewed 0 times
privilege escalationcolumn level privilegeswith checkorganization_idrolerow level security
browserssr

Error Messages

infinite recursion detected in policy for relation "users"

Problem

Multi-tenant Supabase apps often add create policy ... on users for update using (id = auth.uid()) so people can edit their display name. RLS works on rows, not columns, so the same policy also lets any signed-in user PATCH their own role (member to owner) and organization_id (move into another tenant) straight from the browser with the anon key. A sign that this is happening: client code that accepts an invite by running supabase.from('users').update({ organization_id, role }) itself.

Solution

1) Take away table-wide UPDATE and grant only the safe columns: revoke update on public.users from anon, authenticated; grant update (full_name, avatar_url) on public.users to authenticated; Do the same for the tenant table and leave out plan, billing and approval columns. 2) Do any change to role or tenant (invite acceptance, promotions) in a server route that uses the service-role key. That route checks the invite token, its expiry, and that it matches the caller's confirmed email. 3) Another option is a BEFORE UPDATE trigger that raises an error when current_user is 'authenticated' and role or organization_id changed. 4) Use SECURITY DEFINER helpers such as my_org_id() in policies instead of selecting from the users table inside a users policy, which fails with an infinite-recursion error.

Why

Postgres RLS cannot restrict columns. An UPDATE policy with no WITH CHECK reuses the USING expression (id = auth.uid()), and that passes whatever values the new row has. Supabase grants table-wide UPDATE to the authenticated role by default, so every column can be written. The same problem hits tenant tables where owners can write billing or approval columns (plan, trial_ends_at, payout-approval flags): an 'owner can update own org' policy lets them grant themselves a paid plan or skip an anti-fraud gate.

Gotchas

  • UPDATE policies without WITH CHECK reuse USING, so id = auth.uid() never limits the new column values
  • Approval or billing flags kept on a table the tenant owner can update give no protection
  • Code that writes plan='pro' from the browser after a checkout redirect means anyone can get it by visiting the URL

Code Snippets

Column-scoped update grants

revoke update on public.users from anon, authenticated;
grant update (full_name, avatar_url) on public.users to authenticated;
revoke update on public.organizations from anon, authenticated;
grant update (name, phone, logo_url) on public.organizations to authenticated; -- never plan/billing/approval columns

Context

When a multi-tenant Supabase app lets the browser write to profile or tenant tables with the anon key.

Revisions (0)

No revisions yet.