Les erreurs de sécurité au niveau des lignes (RLS) dans les applis Supabase codées par IA, et comment les tester.
Dans une appli Supabase, le navigateur interroge la base avec une clé publique : la sécurité au niveau des lignes (RLS) est la seule chose entre cette clé et vos tables. Les erreurs qui exposent des données sont peu nombreuses : RLS désactivée, politiques à true, règles fondées sur user_metadata, clé service_role dans le code livré, stockage sans politiques.
Faits vérifiés le
Pourquoi une politique manquante expose toute une table
Les applis faites avec Lovable, Bolt et compagnie appellent généralement Supabase directement depuis le navigateur avec supabase-js. La clé utilisée est faite pour être publique. La documentation de Supabase le dit : « n’importe qui peut la lire, elle n’atteint donc que ce que la sécurité au niveau des lignes permet ». N’importe qui peut copier la clé dans votre JavaScript et envoyer ses propres requêtes au point de terminaison REST du projet. Si une table n’a pas de RLS, ou a une politique qui laisse passer toutes les lignes, la requête renvoie la table.
C’est arrivé à grande échelle. La fiche de la CVE-2025-48757 décrit une politique RLS insuffisante dans Lovable jusqu’au 2025-04-15, qui permettait à des attaquants non authentifiés de lire ou d’écrire dans n’importe quelle table des sites générés. Lovable la conteste : selon l’entreprise, chaque client est responsable de protéger les données de son appli. Le compte rendu des chercheurs énumère ce qui était accessible : tables d’utilisateurs avec noms et courriels, clés d’API de tiers, données de paiement et d’abonnement. En février 2026, The Register a rapporté qu’une appli Lovable mise en vitrine exposait 18 697 dossiers d’utilisateurs; le chercheur l’attribuait à l’absence de RLS et à des vérifications d’accès inversées. Lovable a répondu que chaque projet reçoit une analyse de sécurité gratuite avant publication, que ses recommandations n’avaient pas été appliquées et que la base vulnérable n’était pas hébergée par Lovable.
La responsabilité revient à qui publie l’appli. Les vérifications ci-dessous prennent un après-midi.
Les six erreurs
Chacune correspond à une vérification du Security Advisor de Supabase (Database → Advisors), qui la nomme avec le code indiqué.
| Erreur | Ce que permet la clé publique | Code de l’Advisor | Correction |
|---|---|---|---|
RLS désactivée sur une table de public † † † | Lire toutes les lignes avec ?select=*, et insérer, modifier ou supprimer là où anon a le droit | 0013_rls_disabled_in_public | enable row level security, puis écrire les politiques. Les tables créées dans l’éditeur SQL ou par une migration n’ont pas la RLS par défaut; celles du tableau de bord, oui. |
Une politique using (true) † † | L’équivalent de l’absence de RLS pour cette opération. Les politiques permissives se combinent par OU : un seul true annule les plus strictes. | 0024_permissive_rls_policy | La restreindre : (select auth.uid()) = user_id. Garder true seulement pour des données vraiment publiques. |
Des politiques fondées sur user_metadata † † † | Appeler supabase.auth.updateUser({ data: { role: 'admin' } }) et passer toute vérification qui le lit | 0015_rls_references_user_metadata | Mettre les rôles dans app_metadata, que l’utilisateur ne peut pas modifier, ou dans une table que seul le serveur écrit. |
La clé service_role ou secrète dans le navigateur † | Tout. Ce rôle a BYPASSRLS; aucune politique ne s’applique. | Aucun : fouillez le code livré | La garder dans le code serveur seulement. La remplacer si elle a déjà été publiée. |
| Des compartiments de stockage aux politiques trop larges † † | Lister et télécharger tous les fichiers, ou téléverser dans le dossier d’un autre | 0025_public_bucket_allows_listing | Des politiques par dossier d’utilisateur sur storage.objects avec storage.foldername(name). |
Vues et fonctions SECURITY DEFINER † † | Lire par une vue qui s’exécute avec les droits de son créateur, ou appeler une fonction qui s’exécute avec ceux de son propriétaire | 0010_security_definer_view, 0028_anon_security_definer_function_executable | security_invoker = true sur les vues; retirer execute à anon sur les fonctions à droits du propriétaire. |
† Les détails sur les concurrents proviennent des pages publiées par chaque fournisseur aux dates indiquées; les prix et forfaits changent, vérifiez auprès du fournisseur avant de décider. RLS désactivée sur une table de `public` (2026-10-29) · RLS désactivée sur une table de `public` (2026-10-29) · RLS désactivée sur une table de `public` (2026-10-29) · Une politique `using (true)` (2026-10-29) · Une politique `using (true)` (2026-10-29) · Des politiques fondées sur `user_metadata` (2026-10-29) · Des politiques fondées sur `user_metadata` (2026-10-29) · Des politiques fondées sur `user_metadata` (2026-10-29) · La clé `service_role` ou secrète dans le navigateur (2026-10-29) · Des compartiments de stockage aux politiques trop larges (2026-10-29) · Des compartiments de stockage aux politiques trop larges (2026-10-29) · Vues et fonctions `SECURITY DEFINER` (2026-10-29) · Vues et fonctions `SECURITY DEFINER` (2026-10-29)
Étape 1 : lire l’état en SQL
Dans l’éditeur SQL de Supabase, lancez ces trois requêtes. La première liste les tables sans RLS. La deuxième affiche chaque politique avec sa condition : un true ou un user_metadata saute aux yeux. La troisième montre ce que le rôle anon peut toucher. Supabase rappelle que les droits (grants) et la RLS s’appliquent tous les deux : une table doit être accordée pour être joignable, et la RLS limite les lignes.
-- 1. Which tables have RLS on (false = readable by the public key)
select relname, relrowsecurity
from pg_class
where relnamespace = 'public'::regnamespace and relkind = 'r'
order by relrowsecurity, relname;
-- 2. Every policy, with its condition. Look for "true" and user_metadata.
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname in ('public', 'storage')
order by tablename;
-- 3. What the anon role is granted
select table_name, string_agg(privilege_type, ', ') as privileges
from information_schema.role_table_grants
where grantee = 'anon' and table_schema = 'public'
group by table_name;Étape 2 : tester de l’extérieur avec la clé anon
Lire les politiques vous dit ce que vous vouliez. Appeler l’API vous dit ce qui se passe vraiment. Utilisez la même clé publique que l’appli livre, de l’extérieur, comme le ferait un attaquant :
- Chaque table, déconnecté. Tout ce qui est privé doit renvoyer
[]. Une table avec la RLS activée et aucune politique ne renvoie rien : c’est le comportement sûr par défaut. - Deux comptes. Connecté comme B, demandez les lignes de A par identifiant. Essayez ensuite
PATCHetDELETEsur une ligne de A. Faites les tests d’écriture sur une copie du projet, pas sur les données de production. - Le stockage, déconnecté. Dans la console du navigateur sur l’appli en ligne, appelez
supabase.storage.from('<compartiment>').list()pour chaque compartiment. Une liste des fichiers des autres, c’est une fuite. - Le code livré. Cherchez
service_roleet votre clé secrète elle-même dans le JavaScript compilé; la liste de vérification donne la commande.
URL=https://<project-ref>.supabase.co
KEY=<publishable or anon key, copied from the app's bundle>
# Logged out: should return [] or an error for anything private
curl "$URL/rest/v1/profiles?select=*&limit=5" -H "apikey: $KEY"
# Signed in as user B (access token from the browser's session):
# should return only B's rows, never A's
curl "$URL/rest/v1/orders?select=*&user_id=eq.<A's id>" \
-H "apikey: $KEY" -H "Authorization: Bearer <B's access token>"Étape 3 : corriger
Envelopper auth.uid() dans (select …) est la recommandation de Supabase : Postgres l’évalue une fois par instruction plutôt qu’une fois par ligne. Ajoutez to authenticated pour que la politique ne soit même pas évaluée pour les requêtes sans session.
-- Turn RLS on. With no policy, nothing is returned to the public key.
alter table public.orders enable row level security;
-- Owners read and write their own rows
create policy "own orders" on public.orders
for all to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
-- Admin checks read app_metadata, which users cannot edit
create policy "admins read all" on public.orders
for select to authenticated
using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin');
-- Views run with the caller's rights (Postgres 15+)
alter view public.order_summary set (security_invoker = true);Politiques de stockage
Supabase Storage garde la liste de ses fichiers dans storage.objects, et les mêmes règles RLS s’y appliquent. Les téléversements sont bloqués tant qu’il n’y a pas de politique INSERT; les remplacements (upsert) exigent aussi SELECT et UPDATE. Un compartiment public sert n’importe quel fichier à quiconque a son URL : gardez les fichiers privés hors des compartiments publics, peu importe les politiques.
-- Uploads only into a folder named after the user's id
create policy "own folder upload" on storage.objects
for insert to authenticated
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);Postgres ordinaire, Canner compris : même RLS, autre exposition
La RLS est une fonction de Postgres, pas de Supabase, et elle fonctionne pareil sur n’importe quel Postgres, y compris une base Canner. Ce que Canner n’a pas, c’est l’API REST de Supabase devant la base, les rôles anon et authenticated, ni auth.uid(). Une base Canner est joignable de l’intérieur de Canner (votre appli, Data Workshop, les requêtes planifiées) et, si vous activez l’accès externe, depuis les adresses IP que vous autorisez. Il n’y a aucune clé publique à copier.
La plupart des applis avec un vrai service dorsal appliquent donc les accès dans le code serveur. L’équivalent d’une politique manquante, c’est une route qui charge orders where id = $1 sans and user_id = $2. Testez-la de la même façon : deux comptes, on échange l’identifiant.
La RLS peut quand même servir de deuxième couche derrière ces vérifications. Définissez l’utilisateur pour chaque transaction et écrivez les politiques en fonction de lui :
alter table notes enable row level security;
alter table notes force row level security; -- the table owner too
create policy notes_owner on notes
using (user_id = nullif(current_setting('app.user_id', true), '')::uuid);Définir l’utilisateur depuis votre appli
- `set_config(…, true)` ne dure que le temps de la transaction : l’utilisateur ne peut pas déborder sur la requête suivante d’une connexion partagée.
- `nullif(…, '')` : sans utilisateur défini, la politique compare à
NULLet ne renvoie aucune ligne. Sur une connexion réutilisée, le réglage peut revenir sous forme de chaîne vide, ce qui ferait échouer la conversion enuuid;nullifla change enNULL. - Le propriétaire de la table contourne la RLS, sauf avec
FORCE ROW LEVEL SECURITY, et les superutilisateurs et les rôlesBYPASSRLSla contournent toujours. Si votre appli se connecte avec la connexion qui a lancé les migrations, elle est propriétaire des tables. Forcez la RLS, ou donnez à l’appli un rôle qui n’en est pas propriétaire; sur l’instance dédiée,canner db least-privilegele fait en une étape. - RLS forcée et sauvegardes Canner : une sauvegarde prise comme propriétaire s’arrête avec une erreur de sécurité au niveau des lignes sur une table forcée. Sur l’instance dédiée, réglez Les sauvegardes s’exécutent en tant que sur le superutilisateur de l’instance (documentation).
// node-postgres: set the user for this transaction only
await client.query("BEGIN");
await client.query("SELECT set_config('app.user_id', $1, true)", [session.userId]);
const { rows } = await client.query("SELECT * FROM notes WHERE id = $1", [id]);
await client.query("COMMIT");Questions
La clé anon ou publishable de Supabase est-elle un secret?
Non. Elle est faite pour être livrée au navigateur, et Supabase précise qu’elle n’atteint que ce que la sécurité au niveau des lignes permet. C’est la clé secrète, ou service_role, qui ne doit jamais quitter le serveur.
Comment vérifier que la RLS est activée sur toutes mes tables Supabase?
Lancez select relname, relrowsecurity from pg_class where relnamespace = 'public'::regnamespace and relkind = 'r' dans l’éditeur SQL, ou ouvrez Database → Advisors, qui signale les tables de public sans RLS avec le code 0013_rls_disabled_in_public.
Qu’est-ce que la CVE-2025-48757?
Une CVE de 2025 qui décrit une sécurité au niveau des lignes insuffisante dans les sites générés par Lovable, permettant à des utilisateurs non authentifiés de lire ou d’écrire dans leurs tables. Lovable la conteste et dit que chaque client est responsable des données de son appli. De votre côté, la correction est la même : RLS activée, politiques restreintes, tests avec la clé anon.
Peut-on utiliser user_metadata dans une politique RLS?
Non. Les utilisateurs peuvent modifier leur propre user_metadata avec updateUser : une politique qui y lit un rôle peut être franchie par n’importe qui. Supabase recommande app_metadata, que les utilisateurs ne peuvent pas modifier.
Ai-je besoin de la RLS si mon appli a son propre service dorsal?
Pas pour être en sécurité, tant que chaque route vérifie l’utilisateur connecté côté serveur. La RLS est une bonne deuxième couche : définissez l’utilisateur par transaction avec set_config et forcez la RLS pour couvrir aussi le propriétaire de la table.
À lire aussi
- GuideHébergez un artefact Claude sur votre domaine
- GuideOù héberger votre appli Lovable
- GuideOù héberger votre appli Bolt.new
- GuideDéployez votre appli v0 sans Vercel
- GuideSortez votre appli de Replit Deployments
- BlogueL'API REST, la CLI et le serveur MCP de Canner pour agents IA
Hébergez l’appli, gardez les données à Montréal.
Déployez une appli créée par IA avec une base Postgres qui n’a aucune API publique devant elle. Plus dans héberger une appli créée par IA.