En bref
Une vue Postgres s’exécute avec les droits de son propriétaire, sauf si elle est créée avec security_invoker = true. Dans Supabase, le propriétaire est en général postgres, qui contourne la RLS : une vue sur une table protégée renvoie donc des lignes que la table elle-même cache. Sur mon stack local, la clé publique a obtenu 0 ligne de la table et 3 sur 3 d’une vue simple par-dessus ; avec security_invoker = true, la vue en a renvoyé 0 aussi. Dès Postgres 15, activez-le sur chaque vue d’un schéma exposé.
Mon audit de sécurité Supabase gratuit a une règle appelée « Vues qui contournent RLS », et le Security Advisor de Supabase signale la même chose sous le nom security_definer_view. On la manque facilement, parce que la table d’où ça fuit a l’air protégée.
Pourquoi une vue Supabase contourne-t-elle la RLS ?
Une vue est une requête enregistrée. Par défaut, Postgres vérifie les droits et la RLS des tables qu’elle contient en tant que propriétaire de la vue, pas en tant que la personne qui l’appelle. Le guide RLS de Supabase le dit sans détour : les vues contournent la RLS par défaut, parce qu’elles sont en général créées avec l’utilisateur postgres, et une vue sur une table protégée distribue toutes les lignes que ses politiques devaient retenir.
La Data API sert les vues comme des tables. Si la vue est dans un schéma exposé et que anon ou authenticated peut la lire, la clé publique la lit.
Que renvoyait la vue dans mon test ?
Sur un stack Supabase local (CLI 2.109.1, Postgres 17.6, PostgREST 14.14), j’ai créé une table orders avec trois faux clients, activé la RLS sans politique, et une vue order_summary par-dessus avec un droit select pour anon. Puis j’ai appelé les deux avec la publishable key :
| Requête avec la publishable key | Résultat |
|---|---|
GET /rest/v1/orders (RLS activée, aucune politique) |
[] |
GET /rest/v1/order_summary (vue simple) |
les 3 lignes, avec noms et e-mails |
GET /rest/v1/order_summary après security_invoker = true |
[] |
Même table, même clé. Seul le réglage de la vue a changé ce qui revenait.
Comment la corriger ?
Dès Postgres 15, que fait tourner tout projet Supabase actuel, faites vérifier la RLS à la vue en tant qu’appelant :
alter view public.order_summary set (security_invoker = true);
Pour une nouvelle vue, mettez-le dans la définition :
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
L’appelant doit alors aussi avoir select sur la table sous-jacente, et ce sont les politiques de la table qui décident des lignes. Si une vue doit agréger des données que l’appelant ne peut pas voir ligne par ligne, comme un compteur public, gardez-la hors du schéma exposé et servez-la plutôt par une fonction au résultat étroit.
Pour lister les vues de public qui s’exécutent encore en tant que leur propriétaire :
select c.relname as view
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
and not coalesce(
(select bool_or(option = 'security_invoker=true' or option = 'security_invoker=on')
from unnest(c.reloptions) as option), false);
Sur mon stack, avec une vue simple et deux avec security_invoker = true, elle n’a renvoyé que la vue simple.
Les vues matérialisées ne gèrent pas du tout la RLS ; l’audit les signale donc à part, sous « Vues matérialisées ou tables étrangères dans l’API ». Tout le reste de ce qui passe outre les politiques, de la secret key aux fonctions SECURITY DEFINER, est dans ma checklist de sécurité d’une application Lovable, et la façon de prouver les politiques elles-mêmes dans comment tester Supabase RLS. Une politique qui cherche les équipes via une fonction d’aide, sans récursion, est dans Supabase RLS policy : exemples testés via l’API. Une fonction security definer demande un réglage de plus, un search_path fixe ; pourquoi search_path = public ne suffisait pas dans mon test est expliqué dans Supabase Function Search Path Mutable : ce que ça signifie.