TL;DR
raw_user_meta_data indeholder det, browseren sendte til supabase.auth.signUp(). I en produktionsapp læste triggeren handle_new_user den nye brugers rolle derfra, så enhver kunne tilmelde sig med rolle-id’et for Owner og overtage kontoen. Rettelsen: rollen kommer fra en tabel, som kun serveren skriver til, udfyldt før invitationen sendes.
Det skete i tidsregistreringen til et revisionsfirma, en Supabase-app, hvor hver adgangsregel ligger i databasen. Fejlen sad i den trigger, der opretter en profil for hver ny bruger.
Hvad gjorde triggeren?
Forenklet så den sådan ud:
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;
$$;
Invitationsflowet lagde det rigtige role_id derind, så ved normal brug virkede det. Men raw_user_meta_data er options.data fra en tilmelding, og det er browseren, der bestemmer, hvad der står i den:
await supabase.auth.signUp({
email: 'me@example.com',
password: '...',
options: { data: { role_id: '<owner role uuid>' } },
})
Rettighedstjekket returnerede true for ejere, før det så på noget andet, så det ene felt gav fuld kontrol over kontoen. Supabases egen Users-guide siger, at brugermetadata ikke må bruges i RLS-policies eller autorisationslogik, fordi brugeren kan redigere dem uden nogen kontrol.
Hvad rettede det?
Rollen kommer nu fra en privat tabel, som klienten ikke kan røre:
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;
Invitationsfunktionen kører på serveren, og den inviterendes rettighed bliver tjekket. Den skriver først den ventende invitation og opretter først derefter invitationslinket. Triggeren slår rollen op på e-mailadressen og sletter rækken. Navnet kommer stadig fra metadata, hvilket er fint: det er visningsdata.
Rækkefølgen betyder noget. En invitation opretter brugeren med det samme: Supabases Users-guide skriver, at invitation af en ny e-mailadresse opretter en ubekræftet bruger. Triggeren udløses i det øjeblik, så den ventende række skal allerede findes.
Hvordan tjekker du din egen app?
Åbn din handle_new_user-funktion eller en hvilken som helst trigger på auth.users, og søg efter raw_user_meta_data. Navn og avatar derfra er i orden. Rolle, virksomhed, abonnement, tenant eller andet, der giver adgang, skal komme fra en tabel eller fra raw_app_meta_data, som brugeren ikke kan ændre.
Prøv det derefter: tilmeld dig via API’et med role_id eller role: 'admin' i options.data. Den nye konto skal ikke få noget ekstra.
Den slags tjek er første skridt i min metode til at teste RLS. For multi-tenant-apps, se også hvad RLS overser for en bruger i to orgs. De andre tjek, jeg kører på hver Supabase-app, står i min sikkerhedstjekliste til Lovable-apps. Det første, jeg kigger på i dashboardet, er hvilke schemas Data API’et eksponerer. Supabase sikkerhedstjek dækker roller, rettigheder, policies og auth-indstillinger mod 45 OWASP Top 10:2025-regler, ikke hvad en trigger kopierer fra tilmeldingsmetadata, så dette er stadig et manuelt tjek.