Bases de la sécurité
Supabase Row Level Security est activé. La table reste publique.
Activer Supabase Row Level Security ne protège pas une table. Ce sont vos politiques, et celle qui a réparé votre app laisse peut-être entrer tout le monde.
En bref
- Dans Supabase, l'interrupteur et les règles sont deux choses distinctes. Activé sans règle, personne ne passe ; activé avec la mauvaise règle, tout le monde passe.
- La règle qui remet une app cassée en marche est en général celle qui autorise n'importe quelle requête, de n'importe qui.
- Lisez la politique, pas le bouton. Un seul mot décide si un inconnu peut lire votre table.
Vous avez activé Row Level Security parce que quelque chose vous l'a dit : l'advisor dans Supabase, une checklist, le résultat d'un scan, quelqu'un sur un Discord. Le bouton est vert dans votre tableau de bord. Et on vous dit que votre table reste lisible par des inconnus.
Voici la partie que guide après guide raconte de travers : le bouton ne protège rien. Il décide que vos règles seront consultées. Les règles sont autre chose, c'est à vous de les écrire, et la règle la plus rapide à écrire, celle qui remet une app cassée en marche, laisse entrer tout le monde.
Row Level Security est activé dans Supabase. Comment la table reste-t-elle publique ?
Parce que l'activer et décider qui entre sont deux étapes différentes, et seule la première est un bouton.
Voyez le bouton comme quelqu'un que vous placez à la porte. L'actionner ne décide pas qui passe. Il décide que quelqu'un vérifie désormais une liste. Vos politiques sont cette liste. Une liste vide refuse tout le monde ; une liste qui dit « tout le monde » ne refuse personne. Les deux sont Row Level Security activé, et votre tableau de bord affiche le même vert dans les deux cas.
C'est pourquoi le réglage à lui seul répond à très peu de choses, et pourquoi notre scanner ne demande jamais à Supabase s'il est activé. Il interroge la table. Il envoie la requête qu'enverrait un inconnu, avec la clé publique embarquée dans votre app, et regarde si une réponse revient. Il demande un décompte plutôt que les lignes, donc il apprend que la porte s'est ouverte sans lire ce qu'il y a derrière.
La politique qui a réparé votre app est probablement le problème
Au moment où vous activez Row Level Security, votre app cesse d'afficher des données, et ce que vous avez fait ensuite pour la faire remarcher est justement ce qui mérite un regard.
Cette séquence est tout à fait normale, et c'est là que ça dérape. Avec le réglage activé et aucune politique écrite, Postgres, le moteur de base de données sur lequel tourne Supabase, refuse toutes les requêtes par défaut : vos listes reviennent vides et vos écrans restent blancs. Il faut bien mettre quelque chose sur la liste. Si vous avez demandé à Cursor ou à Lovable de réparer ça, ou collé le premier extrait qui faisait disparaître l'erreur, ce que vous avez maintenant ressemble sans doute à ceci :
CREATE POLICY "Enable read access for all users"
ON public.profiles
FOR SELECT
USING (true);
USING (true) est la condition qu'une ligne doit remplir avant que la base de
données ne la livre. Toutes les lignes remplissent true. Il y a là un second
détail facile à dépasser : sans clause TO, une politique s'applique à
public, et public couvre aussi bien les visiteurs connectés que de parfaits
inconnus.
L'app remarche donc, rien n'affiche d'erreur, et la table est exactement aussi lisible qu'avant que vous ne commenciez.
Cela répare la lecture, et seulement la lecture. Si votre app enregistre aussi dans cette table, la chose suivante que vous rencontrez est new row violates row-level security policy, soit le même réglage qui refuse une écriture, et aucune policy de lecture ne la fera disparaître.
Les quatre états possibles d'une table
Deux sont sûrs et deux ne le sont pas, et le bouton ne vous dit pas lesquels.
« Clé publiable » ci-dessous désigne celle qui a sa place dans votre application :
sb_publishable_… dans les projets Supabase récents, anon dans les anciens.
| Row Level Security | La politique | Votre app | Un inconnu avec votre clé publiable Supabase |
|---|---|---|---|
| Désactivé | sans objet, aucune consultée | Marche | Lit toutes les lignes |
| Activé | aucune écrite | Cassée | Ne lit rien |
| Activé | USING (true) | Marche | Lit toutes les lignes |
| Activé | USING (auth.uid() = user_id) | Marche | Ne lit que les siennes |
Les deux colonnes du milieu sont la paire qui piège les gens. Même réglage, même pastille verte, résultats opposés, et la différence tient à un mot dans une règle que presque personne n'ouvre.
Si vous préférez ne pas lire chaque politique vous-même, notre scan gratuit pose à votre base de données en ligne la même question qu'un inconnu et vous dit quelles tables ont répondu. Il prend une vingtaine de secondes et ne demande aucun compte : scanner votre app.
« Authenticated » ne veut pas dire « à vous »
Une politique qui autorise authenticated autorise toute personne ayant un
compte, ce qui, si votre app a un formulaire d'inscription ouvert, revient à
toute personne prête à le remplir.
C'est la version plus subtile de la même erreur, et elle survit à beaucoup de
relectures parce qu'elle a l'air soigneuse. TO authenticated USING (true) se
lit comme une restriction, et c'en est une : elle exclut ceux qui ne se sont
jamais inscrits. Ce qu'elle ne fait pas, c'est empêcher un de vos clients de
lire les lignes d'un autre client, ce qui est en général ce que vous entendiez
par « privé ».
La règle qui fait cela nomme le propriétaire de la ligne :
CREATE POLICY "Users read their own rows"
ON public.orders
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
auth.uid() est celui qui demande. user_id est la colonne de la ligne qui dit
à qui elle appartient. La ligne revient quand les deux correspondent, et reste
où elle est quand ce n'est pas le cas.
Comment vérifier vos propres tables en deux minutes
Ouvrez Supabase, allez dans Authentication → Policies, et lisez l'expression à l'intérieur de chaque politique plutôt que la pastille à côté de chaque table.
Trois choses à repérer :
- Une politique dont la condition est
true. Décidez, table par table, si vous seriez à l'aise avec ces données sur une page ouverte. Pour une liste d'articles publiés, oui. Pour tout ce qui contient une personne, non. - Une politique sans clause
TO. Elle s'applique à tout le monde, connecté ou non, même quand le reste de la règle paraît très précis. - Une table avec le réglage activé, aucune politique, et une app qui marche
quand même. Cette combinaison signifie qu'autre chose atteint vos données,
et l'explication habituelle est une clé secrète (
sb_secret_…, ouservice_rolesur un ancien projet), qui ignore toutes les politiques que vous avez écrites. Quelles clés d'API sont sûres dans votre frontend explique comment la distinguer de la clé sûre.
L'autre vérification vers laquelle on se tourne est d'ouvrir l'app dans une fenêtre privée sans se connecter. Elle vaut le coup, mais sachez ce qu'elle prouve. Votre app décide de ce qu'elle affiche. Votre base de données décide de ce qu'elle livre. Ce sont deux décisions différentes, et un inconnu qui saute vos écrans obtient la seconde.
À quoi cela ressemble quand cela vous arrive
À rien. C'est la forme de ce problème, et c'est pour cela qu'il reste en place des mois.
Aucune erreur n'apparaît dans votre builder. Rien ne ralentit, aucun écran ne casse, aucun e-mail n'arrive. Votre app se comporte exactement comme le jour du lancement, parce que de son côté rien n'a changé : elle avait toujours le droit de lire ces lignes. Ce qui a changé, c'est que tout le monde y a droit aussi, avec la clé embarquée dans le code que votre site envoie à chaque visiteur.
Quand cela remonte, cela remonte de biais. Une cliente demande comment quelqu'un
a su une chose que seule votre app savait. Une liste d'adresses que vous n'avez
jamais publiée réapparaît quelque part. Et si la règle généreuse couvre aussi
l'écriture, FOR ALL au lieu de FOR SELECT, alors n'importe qui peut aussi
modifier et supprimer des lignes : c'est la version que les gens découvrent sous
la forme d'une table soudainement vide.
Les règles glissent aussi. Une migration, un changement de schéma, une autre réparation nocturne qui avait besoin que les données se chargent : chacune peut desserrer une politique sans le dire. C'est pourquoi cela mérite un second regard plus tard, et pas seulement un regard maintenant. Surveiller cela fait partie de ce que fait Reeve Care, même si un rappel dans votre agenda remplit la même fonction.
Que faire cette semaine
Que faire
- Ouvrez Authentication → Policies dans Supabase et lisez la condition de chaque politique, table par table. La pastille sur la table n'est pas la réponse.
- Pour chaque table contenant des personnes, utilisateurs, profils, commandes, messages, vérifiez que la règle nomme le propriétaire de la ligne au lieu d'autoriser tout le monde.
- Remplacez tout
USING (true)sur ces tables par une règle qui compareauth.uid()à la colonne du propriétaire, puis vérifiez que votre app charge encore. - Ajoutez la clause
TOque vous aviez en tête. Une politique sans elle s'applique aux inconnus comme aux visiteurs connectés. - Si une table a le réglage activé, aucune politique, et que votre app affiche quand même ses données, trouvez ce qui le contourne avant de toucher à autre chose.
Commencez par la table qui vous gênerait le plus si elle était une page ouverte, et réglez celle-là aujourd'hui. La checklist sécurité en 10 minutes couvre ce point avec le reste de ce qui mérite un contrôle sur une app fraîchement lancée, et le guide de sécurité Supabase passe en revue ce qui reste couramment ouvert.
FAQ
J'ai activé Row Level Security et mon app n'affiche plus de données. Ai-je cassé quelque chose ?
Non, c'est le réglage qui fait son travail. Avec Row Level Security activé et aucune politique écrite, Postgres, le moteur de base de données sous Supabase, refuse toutes les requêtes par défaut, y compris celles de votre propre app. La solution est d'ajouter une politique décrivant qui doit voir quelles lignes. L'erreur à éviter est d'en ajouter une qui autorise tout le monde, parce que c'est la version qui fait remarcher l'app et laisse la table ouverte.
USING (true) est-il parfois la bonne politique ?
Oui, pour des données réellement publiques. Une table d'articles publiés, un catalogue produits, une liste de lieux sur une carte : elles sont faites pour être lues par tout le monde, et une politique qui l'autorise est correcte. Elle cesse de l'être dès que la table contient des personnes. Demandez-vous si vous seriez à l'aise en publiant le contenu de cette table sur une page ouverte, et laissez la réponse décider de la politique.
Row Level Security me protège-t-il si ma clé secrète a fuité ?
Non. Une clé secrète Supabase (sb_secret_ dans les projets récents, service_role dans les anciens) contourne entièrement Row Level Security, c'est sa raison d'être. Toutes les politiques que vous avez écrites sont ignorées, sur toutes les tables. Si cette clé est dans votre frontend, vos politiques ne font rien pour vous, et la faire tourner est le premier chantier, avant tout travail sur les règles.
Ai-je besoin de Row Level Security si mon app a déjà un écran de connexion ?
Oui. Votre écran de connexion contrôle votre app, et votre app n'est pas le seul chemin vers votre base de données. Supabase donne à chaque projet une adresse web qui répond directement aux requêtes, et la clé pour lui parler se trouve dans le code que votre app envoie à chaque visiteur. Les politiques sont la partie qui s'applique quelle que soit la porte empruntée.