Kort sagt
raw_user_meta_data innehåller vad webbläsaren skickade till supabase.auth.signUp(). I en produktionsapp läste triggern handle_new_user den nya användarens roll därifrån, så vem som helst kunde registrera sig med Owner-rollens id och äga kontot. Åtgärden: rollen kommer från en tabell som bara servern skriver till, ifylld innan inbjudan skickas.
Det här hände i tidrapporteringssystemet för revisionsbyrån, en Supabase-app där varje åtkomstregel ligger i databasen. Buggen satt i triggern som skapar en profil för varje ny användare.
Vad gjorde triggern?
Förenklad såg den ut så här:
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
declare
_role_id uuid := (new.raw_user_meta_data ->> 'role_id')::uuid;
begin
insert into public.profiles (id, full_name, role_id)
values (new.id, new.raw_user_meta_data ->> 'full_name', _role_id);
return new;
end;
$$;
Inbjudningsflödet la rätt role_id där, så vid normal användning fungerade det. Men raw_user_meta_data är options.data från en registrering, och webbläsaren bestämmer vad som ligger i den:
await supabase.auth.signUp({
email: 'me@example.com',
password: '...',
options: { data: { role_id: '<owner role uuid>' } },
})
Behörighetskontrollen returnerade sant för ägare innan den tittade på något annat, så det enda fältet gav full kontroll över kontot. Supabases egen Users-guide säger att användarmetadata inte får användas i RLS-policyer eller auktoriseringslogik, eftersom användaren kan ändra den utan några kontroller.
Vad åtgärdade det?
Rollen kommer nu från en privat tabell som klienten inte kommer åt:
create table private.pending_invites (
email text primary key,
role_id uuid,
invited_by uuid,
created_at timestamptz default now()
);
revoke all on table private.pending_invites from anon, authenticated;
Inbjudningsfunktionen körs på servern, efter att den som bjuder in har fått sin behörighet kontrollerad. Den skriver först den väntande inbjudan och skapar sedan inbjudningslänken. Triggern slår upp rollen via e-postadressen och raderar raden. Namnet kommer fortfarande från metadata, vilket är i sin ordning: det är visningsdata.
Ordningen spelar roll. En inbjudan skapar användaren direkt: Supabases Users-guide noterar att en inbjudan till en ny e-postadress skapar en obekräftad användare. Triggern körs i just det ögonblicket, så den väntande raden måste redan finnas.
Hur kontrollerar du din egen app?
Öppna din handle_new_user-funktion, eller vilken trigger som helst på auth.users, och sök efter raw_user_meta_data. Namn och avatar därifrån är i sin ordning. Roll, företag, plan, tenant eller något annat som ger åtkomst måste komma från en tabell eller från raw_app_meta_data, som användaren inte kan ändra.
Prova sedan: registrera dig via API:et med role_id eller role: 'admin' i options.data. Det nya kontot ska inte få något extra.
Den här sortens kontroll är första steget i min RLS-testmetod. För multi-tenant-appar, se också vad RLS missar för en användare i två organisationer. De andra kontrollerna jag kör på varje Supabase-app finns i min säkerhetschecklista för Lovable-appar. Det första jag tittar på i dashboarden är vilka scheman Data API:et exponerar. Supabase säkerhetskontroll täcker roller, behörigheter, policyer och auth-inställningar mot 45 OWASP Top 10:2025-regler, inte vad en trigger kopierar från registreringsmetadata, så det här är fortfarande en manuell kontroll.