La règle que personne n'a écrite : lire une table Supabase sans Row Level Security
Le défi
Un profil demande deux colonnes à Supabase. Demande toutes les colonnes de chaque ligne. Rapporte la clé API de l'admin.
Ce que tu vas apprendre
- Lire une URL PostgREST : table, liste select, filtres
- Expliquer pourquoi la clé anon de Supabase est publique et ce qui protège réellement les données
- Élargir une requête de deux colonnes d'une seule ligne à toute la table
- Distinguer un nom affiché choisi par l'utilisateur d'un rôle défini côté serveur
- Verrouiller une table Supabase avec RLS, une vue publique et des column grants
Compétences testées
Prérequis
- Ce qu'est une query string
- Ce que sont une table de base de données et ses colonnes
Comment ça marche
Supabase place une API REST (PostgREST) devant Postgres. Le navigateur lui parle directement : l'URL nomme la table (/rest/v1/profiles), les colonnes (select=display_name,avatar_url) et les lignes (id=eq.<uuid>). Chaque requête porte la clé anon du projet, un JWT avec "role":"anon" livré dans le JavaScript de l'application.
Cette clé est publique par conception. Ce qui protège les données, c'est le Row Level Security : des politiques écrites dans Postgres qui décident, par rôle, quelles lignes une requête peut voir. Avec RLS désactivé sur une table que le rôle anon peut lire, l'URL est la seule limite, et c'est le navigateur qui écrit l'URL. Une page qui demande deux colonnes d'une seule ligne, c'est une convention, pas un contrôle.
En 2025, des chercheurs ont découvert que des applications générées avec Lovable, un outil de création d'applications par IA, livraient souvent des tables sans RLS (CVE-2025-48757) : n'importe qui avec la clé publique pouvait lister les utilisateurs, les e-mails et les clés API stockées.
Erreurs fréquentes
- S'arrêter à select=*. Avec le filtre
id=eq.toujours en place, tu obtiens toutes les colonnes de la seule ligne de Sam, et Sam est un membre. Supprime le filtre pour voir les autres lignes. - Prendre la ligne appelée Admin.
display_nameest ce qu'un utilisateur a tapé à l'inscription. Cette ligne a"role":"member"; l'admin est la ligne avec"role":"admin". - Répondre avec la clé de Sam. C'est la première clé que tu vois, mais elle appartient au profil affiché à l'écran.
- Forger un token service_role. Modifier le JWT casse sa signature HS256, et PostgREST répond
PGRST301. Aucune forge n'est nécessaire : la clé anon lit déjà tout. - Retirer le header apikey. La gateway de Supabase refuse la requête avant que PostgREST ne la voie.
Comment s'en protéger
- Active RLS sur chaque table d'un schéma exposé :
alter table profiles enable row level security;Le Security Advisor de Supabase signale les tables où il est désactivé. - Écris des politiques explicites, par exemple chaque propriétaire lit sa propre ligne :
create policy "own row" on profiles for select to authenticated using (auth.uid() = id); - Expose les champs publics via une vue ou une fonction qui ne renvoie que
display_nameetavatar_url, ou révoque les privilèges de colonne pour anon. - Garde les secrets hors des tables lisibles par le navigateur : stocke les clés API hachées, dans un schéma que la Data API n'expose pas, et fais tourner toute clé qui a déjà été lisible.
- Ne décide jamais d'un privilège à partir de champs modifiables par l'utilisateur, comme un nom affiché.