Hopp til innhold

Supabase raw_user_meta_data: slik ble en registrering Owner

Supabase

Vis som MarkdownÅpne i ClaudeÅpne i ChatGPTÅpne i Perplexity

Spørsmålet kopieres også. Hvis chatten åpnes tom, lim det inn.

TL;DR

raw_user_meta_data inneholder det nettleseren sendte til supabase.auth.signUp(). I en produksjonsapp leste triggeren handle_new_user rollen til den nye brukeren derfra, så hvem som helst kunne registrere seg med id-en til Owner-rollen og eie kontoen. Løsningen: rollen kommer fra en tabell som bare serveren skriver til, fylt ut før invitasjonen sendes.

Dette skjedde i timeregistreringssystemet for revisjonsselskapet, en Supabase-app der hver tilgangsregel ligger i databasen. Feilen satt i triggeren som oppretter en profil for hver nye bruker.

Hva gjorde triggeren?

Forenklet så den slik ut:

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;
$$;

Invitasjonsflyten la riktig role_id der, så ved vanlig bruk fungerte det. Men raw_user_meta_data er options.data fra en registrering, og nettleseren bestemmer hva som står i den:

await supabase.auth.signUp({
  email: 'me@example.com',
  password: '...',
  options: { data: { role_id: '<owner role uuid>' } },
})

Tilgangssjekken returnerte sant for eiere før den så på noe annet, så det ene feltet ga full kontroll over kontoen. Supabases egen Users-guide sier at brukermetadata ikke må brukes i RLS-policyer eller autorisasjonslogikk, fordi brukeren kan endre den uten noen kontroller.

Hvor rollen til en ny bruker kom fra. Triggeren handle_new_user før og etter løsningen. Før: alle som registrerer seg: signUp options.data → raw_user_meta_data → handle_new_user → Owner-rolle; Etter: bare inviterte: Invitasjonsfunksjon → pending_invites → handle_new_user → Invitert rolle.
Fra registreringstriggeren i en Supabase-app i produksjon, juni 2026.

Hva løste det?

Rollen kommer nå fra en privat tabell 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;

Invitasjonsfunksjonen kjører på serveren, etter at tilgangen til den som inviterer er sjekket. Den skriver først den ventende invitasjonen og lager deretter invitasjonslenken. Triggeren slår opp rollen via e-postadressen og sletter raden. Navnet kommer fortsatt fra metadata, og det er greit: det er visningsdata.

Rekkefølgen er viktig. En invitasjon oppretter brukeren med en gang: Supabases Users-guide noterer at å invitere en ny e-postadresse oppretter en ubekreftet bruker. Triggeren kjøres i det øyeblikket, så den ventende raden må allerede finnes.

Hvordan sjekker du din egen app?

Åpne handle_new_user-funksjonen din, eller en hvilken som helst trigger på auth.users, og søk etter raw_user_meta_data. Navn og avatar derfra er greit. Rolle, selskap, abonnement, tenant eller alt annet som gir tilgang må komme fra en tabell eller fra raw_app_meta_data, som brukeren ikke kan endre.

Prøv så: registrer deg via API-et med role_id eller role: 'admin' i options.data. Den nye kontoen skal ikke få noe ekstra.

Denne typen sjekk er første steg i RLS-testmetoden min. For multi-tenant-apper, se også hva RLS overser for en bruker i to organisasjoner. De andre sjekkene jeg kjører på hver Supabase-app, står i sikkerhetsjekklisten min for Lovable-apper. Det første jeg ser på i dashbordet, er hvilke skjemaer Data API-et eksponerer. Supabase sikkerhetssjekk dekker roller, tilganger, policyer og auth-innstillinger mot 45 OWASP Top 10:2025-regler, ikke hva en trigger kopierer fra registreringsmetadata, så dette er fortsatt en manuell sjekk.

© 2026 Denys Zaichenko. Opprinnelig artikkel: https://madsalt.tech/no/notes/supabase-raw-user-meta-data-role/