Ce qui a réellement mal tourné
Supabase expose vos tables Postgres via une API REST générée automatiquement. Votre frontend communique avec cette API grâce à la clé anon (aussi appelée clé publishable). Cette clé est faite pour être publique. Elle est livrée dans votre bundle JavaScript, et n’importe qui peut la lire dans les DevTools.
Cette conception ne pose aucun problème, tant que chaque table a la Row Level Security (RLS) activée avec des policies sensées. C’est la RLS qui décide quelles lignes une requête donnée a le droit de voir. Comme le souligne TheCyberSecGuru, en citant la documentation de Supabase elle-même : « Sans RLS, toute personne disposant de votre clé anon peut accéder à toutes les données. »
L’enchaînement des erreurs ressemble donc à ceci :
- Un outil d’IA (ou un humain fatigué) génère create table profiles (...).
- Personne n’ajoute enable row level security.
- L’app fonctionne parfaitement pendant les tests, puisque le bug consiste justement à être trop permissif.
- Quiconque copie la clé anon depuis votre bundle peut exécuter select * sur cette table.
Si vous voulez comprendre le fonctionnement de la RLS, notre guide sur la Supabase Row Level Security couvre les bases. Ci-dessous, nous nous concentrons sur le test et la correction.
Comment vérifier votre app (5 minutes)
1. Regardez-la de l’extérieur, comme l’ont fait les chercheurs. Ouvrez votre site en production, ouvrez les DevTools et cherchez supabase.co dans le JavaScript chargé. Vous y trouverez l’URL de votre projet, avec la clé anon juste à côté. Puis, depuis un terminal, demandez une ligne d’une table que vous considérez comme privée :
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=1" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"Ce que la réponse vous apprend :
- Un tableau vide [] ou une erreur de permission : parfait, les utilisateurs anonymes ne peuvent pas lire cette table.
- De vraies lignes : vous avez le même problème que ces 7 403 sites.
Recommencez pour chaque table contenant des données utilisateurs : profiles, users, messages, orders, bookings, leads, waitlist, etc. Les tables de liste d’attente et de formulaire de contact s’oublient facilement et contiennent presque toujours des adresses e-mail.
Tant que vous êtes dans le bundle, vérifiez aussi que la clé trouvée est bien la clé anon et non la clé service_role. Le rôle de service contourne entièrement la RLS. S’il se trouve dans votre frontend, considérez-le comme compromis et faites-en la rotation immédiatement : tout ce qui est dans votre bundle peut être lu par chaque visiteur et par chaque script tiers de la page, comme les utilisateurs de Polymarket l’ont appris à leurs dépens, et des bots traquent déjà les clés qui ont fuité. Notre article sur la rotation des secrets de votre app sans interruption de service explique la rotation, et les clés API exposées dans le JavaScript côté client détaille ce qui finit souvent d’autre dans les bundles.
2. Vérifiez de l’intérieur. Dans l’éditeur SQL de Supabase, listez chaque table du schéma public et indiquez si la RLS est activée :
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;Toute ligne avec rowsecurity = false est lisible (et peut-être modifiable) avec la clé anon. Le Security Advisor du tableau de bord Supabase les signale aussi. Lisez ses avertissements au lieu de les ignorer.
3. Cherchez les cas « RLS activée, mais ouverte quand même ». Activer la RLS n’est pas la ligne d’arrivée. Listez vos policies :
select tablename, policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public';Méfiez-vous de qual = true sur une policy select qui s’applique à anon ou public. C’est une policy qui dit « tout le monde peut tout lire », ce qui revient à ne pas avoir de RLS du tout. Les outils d’IA génèrent parfois exactement cela pour faire disparaître une erreur.
Comment corriger le problème
Activez la RLS sur chaque table :
alter table public.profiles enable row level security;
alter table public.messages enable row level security;
-- repeat for every table in publicAvec la RLS activée et aucune policy, la table est verrouillée : la clé anon ne récupère rien. C’est un réglage par défaut sûr. Ajoutez maintenant uniquement les accès dont vous avez réellement besoin.
Écrivez des policies restrictives. Une configuration typique « les utilisateurs ne peuvent voir et modifier que leur propre ligne » :
create policy "Read own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);Points à garder en tête :
- Limitez les policies à authenticated, et non à public, sauf si les données sont vraiment publiques (articles de blog, fiches produits).
- Séparez lecture et écriture. Une table peut être lisible publiquement sans risque et exiger malgré tout des insertions strictement verrouillées.
- Les tables publiques laissent quand même fuiter des colonnes. Si une table profiles doit être publique pour afficher des noms, une colonne e-mail ou téléphone dans la même table est publique elle aussi. Déplacez les champs privés dans une table séparée ou exposez une vue ne contenant que les colonnes sans risque.
- Les vues peuvent contourner la RLS. Dans Postgres, les vues s’exécutent par défaut avec les privilèges de leur propriétaire. Créez vos vues avec security_invoker = true pour que la RLS de l’appelant s’applique.
- Le stockage est un sujet à part. Les buckets publics servent les fichiers à quiconque possède l’URL. Les pièces d’identité, factures et avatars téléversés par des utilisateurs privés doivent aller dans des buckets privés avec des storage policies.
Faites-en la norme, pas une opération de nettoyage. L’échantillon des chercheurs couvre dix mois de nouvelles apps, ce qui signifie que cela se reproduit à chaque nouvelle table. Rendez l’oubli difficile :
- Ajoutez une règle aux instructions de projet de votre outil d’IA : chaque create table dans public doit être suivi de enable row level security et de policies explicites.
- Avant de merger, vérifiez que les migrations générées ne contiennent pas de create table sans enable row level security correspondant.
- Relancez le test depuis l’extérieur après chaque modification du schéma, pas seulement au lancement.
Si vous utilisez Firebase plutôt que Supabase, la même catégorie d’erreur y existe sous la forme de règles de sécurité ouvertes. Consultez Firebase Security Rules : arrêtez de laisser votre base de données ouverte.
Si vous avez trouvé des données exposées
Corriger la policy empêche les lectures futures, mais n’efface pas celles du passé. Soyez honnête avec vous-même sur ce qui était accessible et depuis combien de temps. Examinez les logs de l’API Supabase à la recherche de lectures massives inhabituelles depuis des IP inconnues, faites la rotation de tous les secrets stockés dans des tables lisibles, et renseignez-vous sur vos obligations de notification si des données personnelles comme des e-mails, des pièces d’identité ou des informations de paiement ont été exposées. Dans l’UE, les règles du RGPD sur les violations de données s’appliquent probablement.
Checklist express
Avant de considérer le travail comme terminé, assurez-vous que :
- chaque table de public a rowsecurity = true ;
- aucune policy select n’utilise using (true) sur des tables contenant des données privées ;
- les policies sont limitées à authenticated et liées à auth.uid() ;
- les colonnes privées ne se trouvent pas dans des tables lisibles publiquement ;
- les vues utilisent security_invoker = true ;
- seule la clé anon figure dans votre frontend, jamais service_role ;
- les fichiers privés sont dans des buckets de stockage privés ;
- vous refaites le test depuis l’extérieur après chaque modification du schéma.
Vérifiez votre app depuis l’extérieur
Notre checklist de sécurité pour les apps créées avec l’IA couvre les autres points à vérifier avant le lancement. Ce qui dérange dans cette étude, c’est la banalité de la méthode : clé publique, requête en lecture seule, les données reviennent. C’est exactement la vue qu’a un attaquant, et c’est celle que vous devriez vérifier régulièrement.
AppSafe analyse votre app en production depuis l’extérieur. Le scan complet vérifié inclut des contrôles en lecture seule des bases Supabase et Firebase ouvertes et recherche les clés API exposées dans vos bundles JavaScript, et les scans planifiés peuvent vous alerter quand quelque chose change après un déploiement. Curieux de voir à quoi ressemble un rapport ? Jetez un œil au rapport d’exemple.
Lancez dès maintenant un scan gratuit de votre app et découvrez ce qu’un inconnu peut voir avant que quelqu’un d’autre ne le fasse.
