TL;DR
raw_user_meta_data obsahuje to, co prohlížeč předal do supabase.auth.signUp(). V jedné produkční aplikaci z něj trigger handle_new_user četl roli nového uživatele, takže se kdokoli mohl zaregistrovat s id role Owner a převzít účet. Oprava: role pochází z tabulky, kterou zapisuje jen server, vyplněné dřív, než se pozvánka odešle.
Stalo se to v evidenci času pro auditorskou firmu, aplikaci na Supabase, kde každé přístupové pravidlo žije v databázi. Chyba seděla v triggeru, který vytváří profil pro každého nového uživatele.
Co trigger dělal?
Zjednodušeně vypadal takto:
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;
$$;
Pozvánkový tok sem vkládal správné role_id, takže při běžném použití to fungovalo. Jenže raw_user_meta_data jsou options.data registrace a co v nich bude, rozhoduje prohlížeč:
await supabase.auth.signUp({
email: 'me@example.com',
password: '...',
options: { data: { role_id: '<owner role uuid>' } },
})
Kontrola oprávnění vracela pro vlastníky true dřív, než se podívala na cokoli jiného, takže tohle jediné pole dávalo plnou kontrolu nad účtem. Samotný průvodce Users od Supabase říká, že metadata uživatele se nesmějí používat v politikách RLS ani v autorizační logice, protože je uživatel může upravit bez jakékoli kontroly.
Co to opravilo?
Role se nově bere ze soukromé tabulky, na kterou klient nesáhne:
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;
Funkce pozvánky běží na serveru a kontroluje oprávnění zvoucího. Nejdřív zapíše čekající pozvánku a teprve potom vytvoří odkaz. Trigger vyhledá roli podle e-mailu a řádek smaže. Jméno se pořád bere z metadat, což je v pořádku: jsou to údaje k zobrazení.
Na pořadí záleží. Pozvánka vytvoří uživatele okamžitě: průvodce Users od Supabase uvádí, že pozvání nového e-mailu vytvoří nepotvrzeného uživatele. Trigger se spustí v ten okamžik, takže čekající řádek už musí existovat.
Jak zkontrolovat vlastní aplikaci?
Otevřete svou funkci handle_new_user nebo jakýkoli trigger na auth.users a vyhledejte raw_user_meta_data. Jméno a avatar odtud jsou v pořádku. Role, firma, tarif, tenant nebo cokoli jiného, co uděluje přístup, musí pocházet z tabulky nebo z raw_app_meta_data, které uživatel měnit nemůže.
Pak to vyzkoušejte: zaregistrujte se přes API s role_id nebo role: 'admin' v options.data. Nový účet nesmí dostat nic navíc.
Takováto kontrola je prvním krokem mé metody testování RLS. U multi-tenant aplikací se podívejte i na co RLS přehlédne u uživatele ve dvou org. Další kontroly, které spouštím u každé aplikace na Supabase, jsou v mém checklistu zabezpečení aplikace z Lovable. První, na co se v dashboardu dívám, je která schémata Data API vystavuje. Kontrola zabezpečení Supabase pokrývá role, granty, politiky a nastavení autentizace podle 45 pravidel OWASP Top 10:2025, ne to, co trigger kopíruje z metadat při registraci, takže tato kontrola zůstává ruční.