github

RLS Policies Referencing user_metadata in Supabase

Supabase security checks · HIGH

A policy trusts user_metadata, which users can edit themselves.

What goes wrong

In Supabase, user_metadata (stored as raw_user_meta_data in auth.users) can be changed by the signed-in user with supabase.auth.updateUser({ data: { role: 'admin' } }). An RLS policy that checks user_metadata to decide access can be bypassed by a user who edits their own metadata.

How it happens

The policy reads a role or flag from auth.jwt() -> 'user_metadata' instead of app_metadata.

How to find it

Search pg_policies for expressions that mention user_metadata or raw_user_meta_data.

SELECT tablename, policyname, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
  AND (
    qual::text LIKE '%user_metadata%'
    OR qual::text LIKE '%raw_user_meta_data%'
    OR with_check::text LIKE '%user_metadata%'
    OR with_check::text LIKE '%raw_user_meta_data%'
  );

How to fix it

Use auth.jwt() -> 'app_metadata' in the policy instead. app_metadata can only be set server-side, with the service_role key or the Dashboard.

ALTER POLICY "<policy_name>" ON public.<table_name>
  USING ((SELECT auth.jwt() -> 'app_metadata' ->> 'role') = '<role>');

Check your own project

Locksoup runs these checks on your Supabase database in about a minute, with a read-only role, and gives you the SQL to fix what it finds.

Check my project