Une seule table sans RLS, et toute votre base devient publique
Sur Supabase, chaque table exposée via l'API est lisible par n'importe qui tant que la Row Level Security n'est pas activée. Résultat concret : des applications lancées en quelques heures avec l'IA ont laissé fuiter emails, messages privés et données de paiement de tous leurs utilisateurs. Le coupable est presque toujours le même : une RLS Postgres mal comprise, ou tout simplement oubliée.
Dans cet article, vous allez découvrir :
- •Ce qu'est vraiment la Row Level Security de Postgres (et pourquoi ce n'est pas celle de Power BI)
- •Pourquoi la RLS est le piège numéro 1 des applications générées par IA
- •Comment écrire des policies solides, avec des exemples SQL prêts à copier
- •Comment corriger l'erreur « new row violates row-level security policy » sans rouvrir la table
- •La checklist et les deux requêtes d'audit que nous appliquons sur chaque projet Supabase
Sur Supabase, une table sans policy n'est pas « un peu » exposée : elle est entièrement lisible par le public.
La Row Level Security, c'est quoi exactement ?
Attention à un piège dès le départ : si vous cherchez « row level security » sur Google, la moitié des résultats parlent de Power BI. Oubliez-les. Ici, on parle de la RLS de PostgreSQL, le moteur de base de données qui alimente Supabase. Cela n'a strictement rien à voir avec le filtrage de rapports Power BI.
La RLS Postgres (Row Level Security) est une fonctionnalité native de PostgreSQL depuis la version 9.5, sortie en 2016. Oui, Postgres a bien sa propre RLS, et Supabase ne fait que l'exposer : rien à installer, rien à acheter. Le principe : au lieu de contrôler l'accès table par table avec des GRANT, vous contrôlez l'accès ligne par ligne, en fonction de l'utilisateur qui exécute la requête.
Concrètement, chaque policy répond à une question simple pour chaque ligne : « Est-ce que cet utilisateur a le droit de voir (ou de modifier) cette ligne précise ? »
Pourquoi c'est central sur Supabase
Supabase expose automatiquement votre base Postgres via une API REST (PostgREST) et une API temps réel. Votre front-end appelle cette API avec une clé publique (la fameuse anon key), présente en clair dans le bundle JavaScript envoyé au navigateur.
Autrement dit : votre base de données est directement accessible depuis le navigateur de vos visiteurs. La seule barrière entre un attaquant et vos données, c'est la RLS. Pas votre code React, pas vos vérifications côté client : la RLS, au niveau de la base.
Pour comprendre où la RLS s'insère dans l'architecture globale (auth, API, temps réel, storage), consultez notre guide complet sur Supabase qui détaille chaque brique de la plateforme.
Pourquoi la RLS est le piège numéro 1 des apps générées par IA
Le « vibe coding » a démocratisé la création d'applications : on décrit une idée, l'IA génère le schéma de base, le front et les appels API. En quelques minutes, une app fonctionne. Le problème : l'IA génère rarement une RLS correcte.
Voici les trois scénarios que nous voyons le plus souvent en audit :
1. La RLS n'est jamais activée
L'IA crée les tables, branche le front sur Supabase, et tout marche. Sauf que sans ENABLE ROW LEVEL SECURITY, la table est ouverte à la lecture (et parfois à l'écriture) pour n'importe qui possédant la clé anon. Comme cette clé est dans le code du navigateur, n'importe quel visiteur peut lire toute la table depuis la console.
2. Une policy trop permissive
Deuxième cas classique : l'IA active bien la RLS, mais crée une policy USING (true) pour « que ça marche ». Cette condition est toujours vraie, donc elle autorise l'accès à toutes les lignes. C'est exactement comme ne pas avoir de RLS du tout, avec l'illusion d'être protégé.
3. La clé service_role exposée
La service_role bypasse totalement la RLS (c'est son rôle : les tâches serveur de confiance). Certaines apps générées la placent dans le front-end « parce que ça débloque tout ». C'est une catastrophe : n'importe qui peut alors tout lire, tout modifier, tout supprimer.
La RLS ne dispense pas de réfléchir à la sécurité. Elle vous oblige à la penser au bon endroit : dans la base, pas dans le navigateur.
Si vous fiabilisez une application créée avec l'IA, notre guide sur le vibe coding explique comment transformer un prototype généré en produit réellement sécurisé et maintenable.
Comment écrire une policy RLS : le B.A.-BA
Sécuriser une table se fait en deux temps.
Étape 1 : activer la RLS. Tant qu'elle n'est pas activée, aucune policy ne s'applique.
alter table profiles enable row level security;Une fois activée, le comportement par défaut est « tout refuser » : plus personne n'accède aux lignes tant que vous n'avez pas défini de policy. C'est le principe du « deny by default », la bonne base de départ.
Étape 2 : créer les policies. Une policy se compose de deux clauses selon l'opération :
- •
USING (...): condition de visibilité (pour SELECT, UPDATE, DELETE). Elle filtre les lignes que l'utilisateur peut voir ou cibler. - •
WITH CHECK (...): condition de validité à l'écriture (pour INSERT, UPDATE). Elle vérifie que la nouvelle ligne respecte bien la règle.
La fonction magique de Supabase est auth.uid() : elle renvoie l'identifiant de l'utilisateur connecté (issu de son JWT). C'est le pivot de presque toutes vos policies.
-- Chaque utilisateur lit uniquement son propre profil
create policy "read own profile"
on profiles for select
to authenticated
using ( auth.uid() = id );Cette policy dit : un utilisateur authenticated ne peut lire une ligne de profiles que si l'id de la ligne correspond à son propre identifiant. Simple, mais c'est déjà toute la logique.
L'erreur « new row violates row-level security policy »
C'est le message que vous verrez le plus souvent après avoir activé la RLS. Il signifie qu'un INSERT ou un UPDATE a été refusé parce que la nouvelle ligne ne respecte pas la clause WITH CHECK d'une policy (ou qu'aucune policy n'autorise l'écriture pour ce rôle).
Trois causes expliquent la quasi-totalité des cas que nous rencontrons en audit :
- •Aucune policy d'écriture : vous avez créé une policy
for selectmais pas defor insert. Par défaut, tout est refusé. - •Colonne propriétaire mal remplie : la policy exige
auth.uid() = user_id, mais le front n'envoie pasuser_idou envoie l'identifiant d'un autre utilisateur. Solution :default auth.uid()sur la colonne. - •Requête exécutée en anonyme : l'utilisateur n'est pas connecté,
auth.uid()renvoienull, la condition échoue. Vérifiez que le client envoie bien le JWT.
Ne « corrigez » jamais cette erreur avec with check (true) : vous rouvririez la table à tout le monde. L'erreur fait son travail, c'est votre policy ou votre payload qu'il faut ajuster.
La RLS s'articule étroitement avec le système de connexion. Pour bien comprendre d'où vient auth.uid(), lisez notre article sur l'authentification Supabase, qui détaille les JWT, les rôles et la gestion des sessions.
Exemples de policies concrètes
Voici trois patterns que vous rencontrerez dans presque tous les projets.
Table profils : chacun gère la sienne
alter table profiles enable row level security;
create policy "read own profile"
on profiles for select
to authenticated
using ( auth.uid() = id );
create policy "update own profile"
on profiles for update
to authenticated
using ( auth.uid() = id )
with check ( auth.uid() = id );Table articles : lecture publique, écriture par le propriétaire
alter table posts enable row level security;
-- Tout le monde peut lire les articles publiés
create policy "public read published"
on posts for select
to anon, authenticated
using ( status = 'published' );
-- Seul l'auteur peut créer un article à son nom
create policy "insert own posts"
on posts for insert
to authenticated
with check ( auth.uid() = author_id );
-- Seul l'auteur peut modifier ses articles
create policy "update own posts"
on posts for update
to authenticated
using ( auth.uid() = author_id )
with check ( auth.uid() = author_id );Multi-tenant : isolation par organisation
Le cas le plus fréquent en SaaS : chaque utilisateur ne doit voir que les données de son organisation. On s'appuie sur une table de liaison plutôt que sur une simple colonne, pour éviter les fuites entre clients.
alter table documents enable row level security;
create policy "read org documents"
on documents for select
to authenticated
using (
organization_id in (
select organization_id
from memberships
where user_id = auth.uid()
)
);Astuce performance : sur les grosses tables, encapsulez ce type de sous-requête dans une fonction security definer marquée stable, et indexez les colonnes utilisées dans les policies. La RLS mal indexée est une cause fréquente de lenteurs.
À lire aussi : Construire un SaaS avec Supabase : schéma multi-tenant, rôles, facturation et mise en production, avec la RLS comme colonne vertébrale.
Checklist de sécurisation RLS
Avant de mettre une app Supabase en production, vérifiez chaque point de cette liste.
| Point de contrôle | Ce qu'il faut vérifier |
|---|---|
| RLS activée partout | Chaque table exposée à l'API a enable row level security |
Aucune policy USING (true) | Aucune règle n'autorise l'accès à toutes les lignes sans condition |
| service_role côté serveur | La clé service_role n'apparaît jamais dans le front-end |
| WITH CHECK sur les écritures | INSERT et UPDATE valident bien la propriété de la ligne |
| Rôles explicites | Chaque policy précise to anon ou to authenticated |
| Vues et fonctions | Les vues n'exposent pas de données en contournant la RLS |
| Storage | Les buckets Supabase Storage ont aussi leurs policies |
| Tests réels | Vous avez tenté de lire les données d'un autre utilisateur, et échoué |
Trouver les tables sans RLS en une requête
Avant même de relire vos policies, listez ce qui est exposé sans protection. Dans l'éditeur SQL de Supabase :
select schemaname, tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;Si cette requête renvoie ne serait-ce qu'une ligne, vous avez une table lisible (et souvent modifiable) par n'importe qui possédant la clé anon. Pour voir ensuite les policies existantes et repérer les USING (true) :
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;Ces deux requêtes constituent la première passe de notre audit. Elles s'exécutent en quelques secondes et repèrent les expositions les plus courantes avant la mise en production.
Le test qui compte vraiment : connectez-vous avec le compte A, récupérez le token, et essayez de lire les données du compte B via l'API. Si ça échoue, votre RLS tient. Si ça passe, vous avez une faille. Ce test simple attrape la majorité des erreurs.
L'audit RLS ONDEV : ce qu'on vérifie sur chaque projet
Chez ONDEV, la RLS n'est pas une case à cocher en fin de projet : sur chacune de nos applications web sur-mesure, c'est une étape d'audit systématique, table par table, policy par policy.
Sur Wiloq, un SaaS de vestiaire numérique événementiel en production sur Next.js et Supabase (12+ fonctionnalités, 3 interfaces), chaque rôle (organisateur, staff, participant) dispose de policies distinctes. Un participant ne peut jamais lire les données d'un autre événement, même en manipulant l'API à la main.
Sur Swap&Share, plateforme d'échange de compétences bâtie sur React, Node et Stripe, l'isolation multi-tenant repose sur le même principe : une membership contrôle l'accès, pas une simple vérification front-end. Et sur l'application métier livrée à Dassault Aviation (90+ utilisateurs actifs), le cloisonnement des données par périmètre est audité de la même manière.
Notre méthode d'audit RLS suit trois passes :
- 1.Inventaire : lister chaque table exposée et son état RLS (activée ou non).
- 2.Analyse des policies : traquer les conditions permissives, les rôles manquants, les écritures non contrôlées.
- 3.Tests d'intrusion applicatifs : tenter concrètement d'accéder aux données d'un autre utilisateur via l'API.
Cette vérification fait partie de chaque audit de code que nous réalisons sur une application Supabase, qu'elle ait été écrite à la main ou générée avec l'IA : inventaire des tables exposées, lecture de chaque policy, test d'accès croisé entre deux comptes. Le livrable est une liste priorisée des failles avec la policy corrigée pour chacune. Si l'application est déjà en production et que l'audit révèle des fondations trop fragiles, nous enchaînons sur une refonte d'application web sécurisée dès le schéma. Et si vous démarrez, confier la mise en production à une agence Supabase qui maîtrise ces sujets vous évite la fuite de données qui ruine la confiance de vos utilisateurs le jour du lancement.
FAQ : Row Level Security sur Supabase
La RLS de Supabase est-elle la même que celle de Power BI ?
Comment activer la RLS sur une table Postgres ?
alter table ma_table enable row level security;. À partir de là, Postgres refuse tout accès aux lignes tant qu'aucune policy ne l'autorise explicitement. Sur Supabase, vous pouvez aussi l'activer depuis l'interface Table Editor, mais la commande SQL a l'avantage d'être versionnée dans vos migrations. Pensez à l'activer sur chaque table du schéma public, pas seulement sur celles qui vous semblent sensibles.Si j'active la RLS, est-ce que mon application va cesser de fonctionner ?
Que signifie l'erreur « new row violates row-level security policy for table » ?
WITH CHECK d'une policy, ou parce qu'aucune policy d'écriture n'existe pour ce rôle. Vérifiez trois points : une policy for insert (ou for update) existe bien, la colonne propriétaire reçoit auth.uid() (un default auth.uid() évite les oublis côté front), et l'utilisateur est connecté. Ne contournez jamais l'erreur avec with check (true).Comment désactiver la RLS sur une table PostgreSQL, et faut-il le faire ?
alter table ma_table disable row level security;. Mais sur Supabase, désactiver la RLS d'une table du schéma public revient à la rendre lisible par tout visiteur possédant la clé anon. Les seuls cas légitimes sont les tables non exposées à l'API (schéma privé) ou les traitements serveur qui utilisent déjà service_role. Si une table « ne marche pas » avec la RLS, le bon réflexe est d'écrire la policy manquante, pas de désactiver la protection.La clé anon exposée dans mon front-end, c'est un problème de sécurité ?
anon est faite pour être publique. Ce qui protège vos données, ce n'est pas le secret de cette clé, mais vos policies RLS. En revanche, la clé service_role ne doit jamais apparaître côté client : elle contourne toute la RLS.Faut-il aussi vérifier les droits côté application ?
La RLS Postgres suffit-elle pour isoler les clients d'un SaaS multi-tenant ?
organization_id, et une policy vérifie que l'utilisateur appartient à cette organisation via une table de membres. Deux conditions pour que cela tienne : indexer les colonnes utilisées dans les policies, et ne jamais exposer la clé service_role côté client. C'est l'architecture que nous appliquons sur Wiloq, où un participant ne peut jamais lire les données d'un autre événement.La RLS ralentit-elle les requêtes ?
stable. Bien conçue, la RLS a un impact négligeable.Conclusion : la RLS n'est pas une option
Sur Supabase, la Row Level Security n'est pas une fonctionnalité avancée réservée aux gros projets. C'est la barrière fondamentale entre vos données et le premier visiteur curieux. Une table sans policy correcte, et c'est toute votre base qui devient publique, y compris les apps générées à la va-vite avec l'IA.
La bonne nouvelle : quelques policies bien écrites, une checklist rigoureuse et un vrai test d'accès croisé suffisent à passer d'une base grande ouverte à une base réellement cloisonnée.
Vous lancez un projet Supabase ou vous avez un doute sur la sécurité d'une app existante ? Parlons de votre audit RLS : nous vérifions vos policies table par table avant que vos données ne se retrouvent exposées.



