← Retour au blog
SécuritéScanaris Team · 9 min de lecture · October 03, 2026

Lovable et la vulnérabilité RLS (CVE-2025-48757) : pourquoi ton app expose peut-être tes données

CVE-2025-48757 est une vulnérabilité de Lovable classée critique (CVSS 9.3) qui a exposé plus de 170 applications. Le problème : n'importe qui, sans se connecter, pouvait lire, et dans bien des cas modifier, des données de ta base : noms, emails, clés d'API tierces, jusqu'au statut des paiements. La cause profonde n'est pas un bug caché de Lovable mais un schéma que partagent presque toutes les apps « vibe-codées » : confier toute la sécurité de la base de données aux politiques RLS de Supabase… et les laisser incomplètes. Ce guide explique ce qui s'est passé exactement, comment vérifier en cinq minutes si ton app a le même trou, comment le boucher avec du SQL concret et quoi faire si tu as déjà exposé des données.

Ce qui s'est vraiment passé

Lovable construit des apps où le navigateur parle directement à la base de données (Supabase) par des appels REST, avec la clé publique anon. C'est un design sain à condition que chaque table ait des politiques de Row-Level Security (RLS) qui décident quelles lignes chaque utilisateur peut lire ou toucher. La vulnérabilité est apparue parce que beaucoup de projets ont été publiés avec des tables sans RLS ou avec un RLS insuffisant. Un attaquant n'avait qu'à modifier la requête REST, changer un filtre dans l'URL, pour demander « toutes les lignes » d'une table qui aurait dû être protégée.

Pour comprendre pourquoi modifier une URL suffit, il faut savoir comment Supabase fonctionne sous le capot. Devant ta base Postgres se trouve une couche appelée PostgREST qui transforme chaque table en endpoint HTTP : GET /rest/v1/customers?select=* renvoie des lignes de la table customers. La clé anon n'est pas un secret : elle est embarquée dans le JavaScript de ton app et tout le monde peut la voir. La seule chose qui décide quelles lignes sortent de cet endpoint, ce sont les politiques RLS de la table. Sans elles, la réponse, c'est la table entière.

Elle a été signalée en mars 2025 par Matt Palmer et Kody Low, et indépendamment par Danial Asaria, avec une divulgation publique le 29 mai 2025 (analyse technique, fiche CVE). Tout projet Lovable utilisant une base de données créée le 15 avril 2025 ou avant peut être concerné.

Chronologie en bref

  • Mars 2025 : premiers signalements privés à Lovable ; on trouve des apps publiées avec des tables lisibles sans session.
  • 15 avril 2025 : la date limite que Lovable donne pour les projets concernés ; les apps générées après incluent des changements dans la façon de créer les tables.
  • 29 mai 2025 : divulgation publique et attribution du CVE avec un score critique.
  • Ensuite : Lovable ajoute des avertissements de sécurité dans l'éditeur, mais les politiques restent la responsabilité de celui qui publie l'app.

Pourquoi ça te concerne même si tu n'utilises pas Lovable

Ce n'est pas un problème propre à Lovable. Toute app qui parle à Supabase directement depuis le navigateur, que tu l'aies construite avec Bolt, v0, Cursor, Replit ou à la main, repose sur un RLS correct sur chaque table. L'app a l'air parfaite, le login fonctionne, mais la vraie sécurité vit dans la base de données, pas dans l'interface. Cacher un bouton dans le frontend ne protège rien : la requête REST est toujours là pour qui sait où regarder.

Il en va de même pour Firebase avec des règles en « mode test », pour PocketBase sans règles de collection, ou pour n'importe quel backend-as-a-service que le navigateur appelle directement. Le schéma est toujours le même : l'IA qui a généré ton app sait la faire fonctionner, mais écrit rarement les règles qui l'empêchent de fonctionner pour ceux qui ne devraient pas.

Comment savoir si ton app est vulnérable (5 minutes)

Test 1 : la requête en navigation privée

  • Ouvre les DevTools (F12) → onglet Network pendant que tu utilises ton app et observe les appels vers *.supabase.co/rest/v1/. Copie l'une de ces URL en entier (l'apikey voyage dans un header ; dans l'URL tu verras la table et le filtre).
  • Ouvre-la dans une fenêtre de navigation privée, sans session. Si elle renvoie des données, chaque visiteur les obtient aussi.
  • Essaie d'en demander plus : si un appel renvoie tes données, retire le filtre de l'URL (la partie ?user_id=eq.…). S'il rend les lignes d'autres utilisateurs, ton RLS n'est pas efficace.

Test 2 : le tableau de bord Supabase

Ouvre ton projet → Table Editor. Chaque table sans RLS porte un badge rouge « RLS disabled ». Va ensuite dans Authentication → Policies : une table avec le RLS activé mais sans politique refuse tout (bien pour les données, mais ton app cessera de fonctionner), et une avec une politique using (true) est aussi ouverte que si le RLS était désactivé. Le tableau de bord a aussi un Security Advisor (sous Advisors) qui liste exactement ces tables.

Test 3 : une requête SQL

Dans le SQL Editor du tableau de bord, cette requête liste les tables du schéma public (celui que l'API expose) qui n'ont pas le RLS activé :

select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
  and not c.relrowsecurity;

Si elle renvoie une ligne contenant des données réelles, cette table est ouverte à travers l'API en ce moment même.

Comment corriger, étape par étape

1. Active le RLS sur chaque table

Pas seulement les « importantes ». La règle, c'est refuser par défaut : sans politique qui l'autorise, personne n'entre. Pour chaque table du schéma public :

alter table public.customers enable row level security;

Une fois fait, ton app cesse de voir des données jusqu'à ce que tu écrives des politiques. C'est normal : la porte est maintenant fermée et tu vas n'ouvrir que ce qui est nécessaire.

2. Écris des politiques explicites par opération

Le schéma le plus courant est « chaque utilisateur ne voit et ne modifie que ses propres lignes », en comparant auth.uid() avec la colonne qui stocke le propriétaire. Pour une table customers avec une colonne user_id :

create policy "customers: lire les siens"
  on public.customers for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "customers: insérer les siens"
  on public.customers for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

create policy "customers: modifier les siens"
  on public.customers for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "customers: supprimer les siens"
  on public.customers for delete
  to authenticated
  using ((select auth.uid()) = user_id);

Trois détails font la différence : to authenticated laisse les visiteurs anonymes dehors ; with check empêche quiconque d'insérer ou de modifier des lignes au nom de quelqu'un d'autre (sans lui, un utilisateur pourrait créer des données avec le user_id d'un autre) ; et envelopper auth.uid() dans un select fait que Postgres l'évalue une fois par requête au lieu d'une fois par ligne, ce qui, sur de grosses tables, multiplie les performances. Ajoute un index sur user_id si tu n'en as pas.

3. N'utilise jamais la clé service_role dans le frontend

Elle contourne entièrement le RLS : avec elle, chaque politique que tu viens d'écrire n'existe plus. Cette clé ne vit que sur le serveur (Edge Functions, ton propre backend, un worker). Si elle s'est un jour retrouvée dans du code client, considère-la comme compromise et fais-la tourner depuis le tableau de bord. Supabase propose aussi désormais de nouvelles clés (sb_publishable_… pour le navigateur et sb_secret_… pour le serveur) qui rendent la séparation plus évidente.

4. Re-teste

Répète le test en navigation privée : la requête sans session doit renvoyer une liste vide ou une erreur 401. Connecte-toi ensuite avec deux comptes différents et confirme qu'aucun ne voit les données de l'autre. Si tu as des tests automatisés, c'est le cas qui en mérite un.

Erreurs courantes pendant la correction

  • Des politiques avec using (true). Les assistants IA les proposent souvent « pour que ça marche ». Elles laissent la table aussi ouverte que sans RLS du tout.
  • Oublier with check. Ça protège les lectures mais autorise les écritures au nom de quelqu'un d'autre.
  • Des tables « catalogue » avec des données personnelles. Une table de profils publique censée afficher des noms finit par exposer des emails ou des numéros de téléphone. N'expose que les colonnes nécessaires via une vue.
  • Des vues qui contournent le RLS. Dans Postgres, une vue s'exécute avec les permissions de celui qui l'a créée. Créée depuis le tableau de bord, elle peut renvoyer des lignes que les politiques de la table de base interdiraient. Crée les vues avec security_invoker = true (Postgres 15 ou plus) ou vérifie-les une par une.
  • Un Storage sans politiques. Les buckets ont leurs propres règles. Un bucket public sert n'importe quel fichier à quiconque possède l'URL ; les documents privés vont dans des buckets privés avec des politiques par utilisateur.
  • Ne corriger que la production. Si tu gardes un projet de développement avec des données copiées, il est tout aussi exposé.

Si tu as déjà exposé des données

Ferme d'abord la porte (les étapes ci-dessus). Fais ensuite tourner chaque clé qui se trouvait dans des tables lisibles : clés d'API tierces, tokens, webhooks. Consulte les logs de l'API dans le tableau de bord Supabase pour estimer ce qui a été lu et depuis quand. Si les données incluent des informations personnelles d'utilisateurs européens, le RGPD t'oblige à évaluer si l'autorité de protection des données doit être notifiée sous 72 heures et, dans les cas à haut risque, les utilisateurs eux-mêmes. Documente ce que tu as trouvé et ce que tu as fait : on te le demandera si quelqu'un vient frapper à la porte.

Un prompt pour corriger avec ton IA

Si tu travailles avec Cursor, Claude ou le chat de Lovable lui-même, ce prompt donne en général un bon résultat : « Liste toutes les tables du schéma public de mon projet Supabase. Pour chacune, active le Row-Level Security et écris des politiques séparées pour select, insert, update et delete restreintes au rôle authenticated, de sorte que chaque utilisateur n'accède qu'aux lignes dont la colonne user_id est égale à (select auth.uid()). Utilise with check sur insert et update. Dis-moi quelles tables n'ont pas de colonne propriétaire pour que je décide celles-là à la main. N'utilise jamais using (true). »

Comment Scanaris le vérifie

Scanaris scanne ton site en lecture seule et détecte l'empreinte de ce schéma : endpoints Supabase joignables depuis le navigateur, CORS permissif, clés et tokens exposés dans ton JavaScript, et données que l'app sert sans session. Si tu fournis une connection string en lecture seule, il audite aussi table par table si le RLS est activé et si des politiques existent. Colle ton URL et il te dit, en quelques secondes et par ordre de gravité, ce qui est à la vue de tous, avant que quelqu'un d'autre ne le trouve.

Questions fréquentes

Lovable a-t-il déjà corrigé le problème ?

Lovable a changé sa façon de générer les tables et ajouté des avertissements, mais une app publiée avant le 15 avril 2025 reste telle que tu l'as laissée : les politiques ne s'ajoutent pas toutes seules. Et dans tout projet, nouveau ou ancien, chaque table que tu crées ensuite a besoin des siennes.

Activer le RLS casse-t-il mon app ?

L'activer sans politiques, oui : tout est refusé. C'est le comportement correct. Écris les politiques de l'étape 2 et l'app refonctionne, mais seulement pour ceux qui le doivent.

Puis-je protéger la table depuis le frontend ?

Non. Le frontend, c'est du code qui tourne sur l'ordinateur du visiteur et qui peut être modifié à volonté. La seule chose qu'on ne peut pas contourner, ce sont les règles appliquées par la base de données.

Et si mon app n'a pas d'utilisateurs, juste une table de contenu public ?

Alors une politique de lecture publique est légitime : for select to anon, authenticated using (true). Ce qui ne doit pas exister, c'est une politique d'écriture ouverte ou des colonnes avec des données personnelles dans cette table.

Comment se porte ton site ?

Passe-lui Scanaris et vérifie en 30 secondes, c'est gratuit.

Analyser mon site

Vérifications liées

Guides de sécurité par stack