En bref
Une application Lovable est aussi sûre que sa base Supabase, et « la RLS est activée » prouve très peu de choses. Testez avec deux utilisateurs dans deux entreprises, gardez les rôles hors des métadonnées utilisateur, gardez la clé secrète hors du navigateur, et vérifiez séparément les droits, les vues, le stockage, les fonctions et les limites de débit, car chacun contourne vos politiques à sa manière. Voici la liste que j’applique, le prompt à coller dans Lovable ou Claude, et huit contrôles ajoutés après que l’application Lovable d’un client m’a montré ce que les douze premiers ratent.
Je suis Denys, ingénieur full-stack et IA. Ce mois-ci, j’ai réparé l’application d’un client construite avec Lovable. Tout tournait sur Supabase, comme d’habitude, et il y avait tant de corrections de sécurité qu’on aurait dit que l’application en était faite. Voici la liste que je parcours désormais sur chaque projet Lovable ou Supabase avant de dire que les données clients sont protégées.
Lovable est-il sûr ? Les problèmes de sécurité connus
La plateforme construit et héberge votre application, mais sa sûreté dépend surtout de la base Supabase qui la soutient, parce que l’application lit et écrit dans cette base directement depuis le navigateur. Le dossier public montre les deux types de problèmes :
| Quand | Ce qui s’est passé | Source |
|---|---|---|
| Mars 2025 | CVE-2025-48757 : un scan de 1 645 applications vitrines de Lovable a trouvé 303 endpoints dans 170 d’entre elles qui servaient des données à quiconque avait la clé publique, car la RLS manquait ou était trop lâche | Divulgation de Matt Palmer, mai 2025 |
| Février 2026 | Une application hébergée sur Lovable a exposé 18 697 fiches d’utilisateurs ; un contrôle d’accès dans ses fonctions laissait entrer les visiteurs anonymes et bloquait les utilisateurs connectés. Lovable a indiqué qu’une partie du code n’avait pas été générée par Lovable | The Register, 27 février 2026 |
| Du 3 février au 20 avril 2026 | Une régression de la plateforme a permis à d’autres utilisateurs de Lovable de lire l’historique de chat et le code source de projets publics | Article de Lovable sur l’incident, 22 avril 2026 |
Les deux premiers sont des failles au niveau de l’application, du genre que les contrôles ci-dessous détectent. Le troisième venait de Lovable lui-même et Lovable l’a corrigé ; votre part est de garder les clés hors des conversations et du code (contrôle 3), et de renouveler celles qui s’y trouvaient. Lovable lance un scan de sécurité avant la publication, mais y donner suite vous revient, et à la publication de la CVE ce scan vérifiait qu’une politique existait, pas qu’elle était correcte. Se demander si Lovable est sûr revient donc à savoir si votre base passe les contrôles ci-dessous.
Pourquoi « la RLS est activée » ne suffit-il pas dans une application Lovable ?
Une application Lovable parle à la base de données directement depuis le navigateur. Il n’y a pas de backend à vous entre les deux : la page appelle l’API de données de Supabase avec une clé publique, et Postgres décide de ce qui revient (les schémas que cette API sert relèvent d’un réglage à part, traité dans Supabase exposed schemas). Toute règle qui protège les données clients doit donc vivre dans la base, et la base a plusieurs portes.
La row level security en est une. Les droits (grants) décident si un rôle peut toucher une table, les politiques décident quelles lignes, et trois choses contournent entièrement les politiques : la clé secrète, les vues créées sans security_invoker et les fonctions SECURITY DEFINER. Ce n’est pas théorique. Le propre guide de sécurité de Lovable demande de vérifier les politiques RLS qu’il met en place, et la CVE-2025-48757 ci-dessus montre ce qui arrive quand personne ne le fait.
Le prompt : à coller dans Lovable ou Claude
Vous pouvez le coller tel quel. Vérifiez ensuite le résultat vous-même : l’IA adore dire « terminé » quand ce n’est pas vrai, et un prompt qui fait disparaître une erreur peut y arriver en élargissant l’accès.
Audite ce projet côté sécurité, corrige ce que tu trouves, dis-moi ce que tu as changé :
1. Teste avec deux utilisateurs dans deux entreprises. L'utilisateur A essaie de lire, modifier, supprimer les enregistrements de B par id, via l'API, la recherche et les exports. Tout doit échouer.
2. RLS sur chaque table, les politiques vérifient le propriétaire de la ligne ou l'entreprise. « Connecté » ne suffit pas.
3. La clé service role jamais dans le frontend. Les edge functions qui l'utilisent vérifient l'utilisateur et filtrent elles-mêmes par entreprise, la clé contourne la RLS.
4. Rôles, offre, id d'entreprise jamais depuis les métadonnées utilisateur, les utilisateurs peuvent les modifier. Garde-les dans une table que seul le serveur écrit.
5. Quand une requête envoie deux id (projet + tâche), vérifie que la tâche appartient à ce projet.
6. Retire tous les droits sur les tables à anon et authenticated, redonne seulement ce dont les politiques ont besoin. La RLS n'arrête pas TRUNCATE. Chaque migration qui crée une table inclut son GRANT : à partir du 30 octobre 2026, Supabase n'accorde plus automatiquement de droits sur les nouvelles tables, et l'application reçoit l'erreur 42501.
7. Toutes les vues en security_invoker = true, sinon elles contournent la RLS.
8. Buckets de stockage : les utilisateurs lisent et écrivent uniquement leurs propres fichiers, aucun bucket public pour des données privées.
9. Fonctions SECURITY DEFINER : NULL doit signifier refuser, pas autoriser.
10. Limites de débit sur connexion, inscription, réinitialisation, OTP, stockées en base, pas en mémoire.
11. Valide toutes les entrées côté serveur.
12. Lance le Security Advisor de Supabase, corrige chaque avertissement.
Les 12 contrôles, et comment prouver chacun
1. Deux utilisateurs dans deux entreprises
Créez l’utilisateur A dans l’entreprise 1 et l’utilisateur B dans l’entreprise 2, chacun avec quelques enregistrements. Connectez-vous en tant que A et essayez de lire, modifier et supprimer les enregistrements de B par id, via l’API, via la recherche et via chaque export. Chaque tentative doit ne rien renvoyer ou renvoyer une erreur. C’est le test qui attrape l’essentiel de ce qui suit, car il vérifie ce que fait l’attaquant, pas ce que vous avez écrit.
Ajoutez un troisième utilisateur qui appartient aux deux entreprises. Une politique de la forme « l’appelant est membre de l’entreprise de la ligne » laisse cet utilisateur lire les lignes de l’entreprise 2 en travaillant dans l’entreprise 1. Sur une autre application Supabase, ma sonde avec un tel utilisateur a renvoyé le document de l’autre entreprise, jusqu’à ce que la requête filtre aussi par l’entreprise de la requête. J’en ai fait une note : un utilisateur dans deux orgs que la RLS n’isolait pas.
2. RLS sur chaque table, limitée au propriétaire ou à l’entreprise
auth.uid() is not null signifie « n’importe quel utilisateur connecté », ce qui, sur une page d’inscription publique, veut dire tout le monde. Chaque politique compare le propriétaire ou l’entreprise de la ligne à l’appelant. Listez les tables qui ont encore la RLS désactivée :
select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
3. La clé secrète jamais dans le frontend
La clé service role, et la clé plus récente sb_secret_, contournent toutes les politiques. Cherchez les deux dans le JavaScript compilé et faites tourner toute clé qui a déjà été livrée. Les edge functions qui l’utilisent doivent vérifier elles-mêmes qui est l’utilisateur et filtrer par entreprise, car avec cette clé rien d’autre ne le fera.
4. Rôles, offre et id d’entreprise jamais depuis les métadonnées utilisateur
raw_user_meta_data est ce que le client a envoyé à signUp, et les utilisateurs peuvent le modifier ensuite. Sur une autre application, le trigger de nouvel utilisateur y lisait le rôle d’invitation, si bien que quiconque s’inscrivait pouvait transmettre le rôle d’owner et devenir Owner. Gardez rôles, offres et id d’entreprise dans une table que seul le serveur écrit, ou dans app_metadata. Toute l’histoire : un rôle tiré des métadonnées d’inscription.
5. Deux id dans une requête : vérifier qu’ils vont ensemble
Quand une requête porte un id de projet et un id de tâche, autoriser le projet ne suffit pas. Un utilisateur qui a des droits sur le projet A envoie A plus un id de section du projet B, et la ligne atterrit dans B. La RLS ne l’attrape pas, car l’insertion enregistre l’id de projet de l’attaquant lui-même, ce qui est exactement ce que vérifie la politique. Vérifiez que l’enfant appartient au parent avant d’écrire.
6. Droits : retirer, puis redonner seulement le nécessaire
Les droits et les politiques sont deux couches distinctes. Ajouter des politiques ne reprend aucun droit, c’est pourquoi le guide RLS de Supabase conseille de retirer puis de redonner explicitement. Sur un autre projet, l’image supabase/postgres 17.6.1 avait donné à anon et authenticated TRUNCATE, REFERENCES, TRIGGER et MAINTAIN sur chaque table de public, et aucune politique ne s’applique à TRUNCATE. L’API de données n’a pas de point d’accès TRUNCATE, ce n’est donc pas un effacement en une requête, mais aucun rôle client n’en a besoin, et toute fonction qui exécute du SQL avec ce rôle en hérite.
revoke all on public.orders from anon, authenticated;
grant select, insert, update on public.orders to authenticated;
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by table_name;
Une date change ce contrôle. Supabase cesse d’accorder automatiquement des droits sur les nouvelles tables : pour les nouveaux projets depuis le 30 mai 2026, et pour tous les projets existants à partir du 30 octobre 2026, y compris ceux créés par Lovable. Ensuite, une table que Lovable ou une migration crée sans GRANT explicite est invisible pour l’application : la requête échoue avec 42501 et la fonctionnalité qui utilise la table casse. Chaque migration qui crée une table porte donc désormais ses droits, à côté de enable row level security et des politiques.
Deux choses ne changent pas. Les tables existantes gardent leurs droits, donc retirer ce dont vous n’avez pas besoin reste votre travail. Et le changement ne reprend que select, insert, update et delete : dans le projet Lovable, avec les nouveaux réglages par défaut, les nouvelles tables arrivaient encore avec TRUNCATE, REFERENCES, TRIGGER et MAINTAIN pour anon et authenticated. Tout retirer d’abord, puis accorder, reste l’ordre sûr.
7. Chaque vue avec security_invoker = true
Une vue s’exécute avec les droits de son propriétaire, et dans Supabase le propriétaire est généralement postgres ; une vue sur une table protégée distribue donc chaque ligne que les politiques retiennent. À partir de Postgres 15 :
alter view public.customer_summary set (security_invoker = true);
8. Stockage : ses propres fichiers seulement, aucun bucket public pour des données privées
Un bucket public sert n’importe quel fichier à quiconque a l’URL. Les fichiers privés vont dans un bucket privé, avec des politiques sur storage.objects qui vérifient le propriétaire ou l’entreprise, généralement à partir du premier dossier du chemin. Après le déplacement d’un projet, comptez les politiques de stockage actives : sur une application, tous les envois échouaient après un déplacement parce que la migration des politiques n’avait jamais tourné et que la table avait la RLS activée avec zéro politique.
9. Fonctions SECURITY DEFINER : NULL signifie refuser
En PL/pgSQL, if not (has_permission or assignee_id = auth.uid()) ne lève rien quand assignee_id est null : toute la condition est null et if l’ignore. La même expression est sûre dans une politique, qui traite null comme un refus. Sur une application, cela laissait un rôle dont toutes les permissions étaient désactivées agir sur les 3683 tâches, parce qu’aucune n’avait d’assigné. Enveloppez la condition dans coalesce(…, false). Détails : un NULL dans un IF NOT PL/pgSQL a sauté le contrôle.
10. Limites de débit stockées dans la base
Limitez la connexion, l’inscription, la réinitialisation du mot de passe et l’OTP, et gardez le compteur dans Postgres. Dans l’application Lovable, les limites vivaient dans une variable à l’intérieur de l’edge function. J’ai sondé le projet hébergé le 17 octobre 2026 : chaque requête s’exécutait dans un isolate neuf, si bien que le compteur en mémoire valait toujours 1 et que la limite ne pouvait jamais se déclencher. Le supabase functions serve local réutilise l’isolate, donc le même code passait tous les tests en local.
La clé compte autant que le stockage. Sur la plateforme hébergée, le dernier saut de X-Forwarded-For est le propre proxy de Supabase : s’en servir comme clé met tous les appelants dans un seul compartiment, et x-client-ip est fixé par le client. Dans ma sonde, cf-connecting-ip portait le vrai client. La correction a été une fonction SECURITY DEFINER claim_rate_limit(bucket, key, max, window) que seul le service role peut exécuter, sérialisée par un advisory lock, qui stocke un hash de l’IP ou de l’id utilisateur plutôt que la valeur brute.
11. Valider toutes les entrées côté serveur
Quoi que vérifie le formulaire, l’API accepte tout ce que le client envoie. Longueurs, formats, enums et appartenance se vérifient dans la base (contraintes, triggers) ou dans l’edge function, jamais seulement dans React.
12. Le Security Advisor de Supabase, chaque avertissement
Ouvrez Advisors, puis Security Advisor, et corrigez chaque constat. Il signale les tables sans RLS, les vues qui contournent la RLS, les politiques qui lisent les métadonnées utilisateur, les politiques permissives using (true) et les fonctions dont le search_path est modifiable. Il ne voit pas les contrôles 1, 5, 9 ni 10, car ils relèvent de la logique, pas de la configuration.
Qu’est-ce que l’application Lovable m’a appris que les 12 ratent ?
Ces points viennent de l’application du client et de ses revues. Aucun n’apparaît dans un scanner.
- Des champs que le client ne doit pas fixer. Le client envoyait lui-même
status: 'approved', ce qui contournait la modération. Une politique UPDATE dont lewith checkest plus souple que sonusingpermet aussi à un utilisateur de déplacer une ligne vers un autre propriétaire. Une politique ne peut pas calculer une valeur : un triggerBEFORE INSERT OR UPDATEredérive doncstatus, le rôle, le prix et l’entreprise à partir des données du serveur et ignore ce que le client a envoyé. - Tous les chemins d’écriture, pas seulement l’insertion principale. Une fois l’insertion principale corrigée, le même contournement se trouvait dans les pièces jointes, les commentaires, la restauration d’un élément supprimé logiquement et les compteurs. Listez chaque écrivain d’une colonne protégée avant de corriger le premier.
- Des listes d’autorisation, pas des listes d’interdiction. Les enregistrements falsifiés ont demandé cinq tours de revue, parce que chaque correction bloquait une combinaison et en laissait une voisine ouverte. Un trigger qui n’autorise que les combinaisons listées de type et de statut a fermé toute la classe d’un coup.
- Les fonctions sont aussi une API. Chaque fonction de
publicest appelable via/rpcpar tout rôle ayant EXECUTE, et la RLS ne s’applique pas aux fonctions. Donnez à chaque fonctionSECURITY DEFINERunset search_path = '', retirez EXECUTE àpublic,anonetauthenticated, et accordez-le uniquement au rôle qui en a besoin. - Retirer EXECUTE peut casser des pages publiques. Postgres vérifie EXECUTE sur chaque fonction utilisée par chaque politique qui s’applique au rôle, même dans une branche qui ne s’exécuterait jamais. Une fois qu’
anona perdu EXECUTE suris_admin(), toute lecture anonyme a échoué avec 42501. Les politiques réservées aux admins vontto authenticated, et les lectures publiques ont leur propre politique sans la branche admin. - Inscription avec confirmation par e-mail activée. Avec la confirmation activée,
signUpne renvoie pas de session : toute écriture de profil juste après s’exécute donc en tant qu’anonet n’écrit rien, sans erreur. Les projets Lovable ont souvent la confirmation automatique activée, ce qui masque le bug. Passez les champs danssignUp({ options: { data } }), laissez le trigger de nouvel utilisateur les valider et les écrire, et traitez ces métadonnées comme non fiables (contrôle 4). - Des tests qui passent pour la mauvaise raison. La stack locale et le projet hébergé avaient des droits par défaut différents : « anon ne peut pas insérer » passait donc en local parce que le droit manquait, pas parce que la RLS fonctionnait. Un test négatif n’a de sens que si le droit positif existe. Comparez
pg_default_aclen local et en production. - La base en production, pas les fichiers de migration. Après une migration ou un déplacement de projet, comparez ce qui est réellement en place :
pg_policies,pg_get_functiondefpour chaque garde-fou, et les droits ci-dessus. Sur une autre application, une nouvelle migration a recopié l’ancien corps d’une fonction et annulé discrètement une correction ultérieure.
Une app Lovable est-elle conforme au RGPD ?
Lovable ne rend pas une app conforme ou non conforme : c’est ce que l’app fait des données personnelles qui décide, et un audit de sécurité n’y répond pas. Voici les six failles que je vérifie dans une app Lovable Cloud, chacune avec sa correction :
- Lister chaque sous-traitant. Lovable Cloud fait tourner la base, l’authentification, le stockage et les fonctions sur Supabase, Lovable héberge le site et peut envoyer les e-mails de connexion, et le Lovable AI Gateway transmet du texte à un fournisseur de modèles. Ajoutez chaque service d’e-mail, d’IA ou de scraping que vos fonctions appellent. Chacun qui reçoit des données personnelles exige un contrat de sous-traitance (DPA), et un fournisseur hors de l’UE exige en plus un mécanisme de transfert. La région d’hébergement n’apparaît nulle part dans le code : demandez-la.
- Supprimer un utilisateur qui a du contenu. Les clés étrangères de ce qu’il a écrit peuvent bloquer la suppression, et une demande d’effacement ne peut alors pas aboutir. Passez ces clés en cascade ou en set null, ajoutez un trigger de suppression qui retire aussi ses journaux d’e-mails et ses jetons de connexion, et gardez un test de base de données qui supprime un tel utilisateur.
- Fixer une durée de conservation. Journaux d’e-mails, suivi des vues, files d’e-mails en échec et lignes supprimées logiquement gardent souvent des données personnelles sans date de fin. Une tâche planifiée qui supprime ces lignes définitivement après un délai fixe, avec leurs fichiers, règle l’essentiel.
- Suivi des e-mails sur consentement uniquement. Enregistrer ouvertures et clics par destinataire exige un consentement selon les règles ePrivacy : un indicateur de consentement, désactivé par défaut, horodaté par le serveur et non par le client.
- Garder les adresses hors des journaux des fonctions. Les journaux des edge functions ne sont pas nettoyés : un
console.logavec l’adresse d’un destinataire l’y laisse. Journalisez des identifiants et des nombres. - Activer la confirmation par e-mail. Avec l’auto-confirmation, n’importe qui peut s’inscrire avec une adresse qui ne lui appartient pas et faire entrer l’adresse d’une vraie personne dans l’app. Le point 6 de la liste ci-dessus couvre le côté inscription.
Pour vérifier votre app : cherchez les clés d’API dans les fonctions pour lister les services, supprimez un utilisateur de test qui a du contenu, cherchez @ dans les journaux des fonctions et demandez à chaque fournisseur son contrat de sous-traitance. Ce n’est pas un avis juridique.
Comment éviter que cela revienne ?
Chaque contrôle ci-dessus est une requête ou un test : gardez-les dans le dépôt et lancez-les avant chaque déploiement. Les tests pgTAP de Supabase couvrent droits, politiques et fonctions ; le test des deux entreprises va à côté. Si votre application a des rôles et un écran de permissions, un test par politique ne suffit pas non plus : je prouve chaque case de permission sur une copie de la production, comme décrit dans comment tester Supabase RLS, où 30 cases sur 87 se sont révélées sans effet. L’Audit de sécurité Supabase gratuit applique 46 règles OWASP Top 10:2025 aux rôles, droits, politiques et paramètres d’authentification d’un projet : servez-vous-en pour le premier passage ; le test des deux entreprises et tout ce qui touche à votre interface restent manuels.
Ce que cette liste ne couvre pas : paiements et webhooks, secrets dans votre historique Git, en-têtes de sécurité de l’hébergeur, sauvegardes. Ils comptent, mais c’est une autre checklist.
Si vous avez hérité d’une application Lovable ou Supabase et que vous ne savez pas lesquels de ces contrôles elle passe, cet audit est le point de départ d’un sauvetage de projet. Le cas du suivi du temps pour un cabinet d’audit montre à quoi ressemblent les contrôles de rôles sur une application en production.
Une faille que cette liste ne couvre pas ?
Je serais ravi de la connaître. Je compare volontiers nos façons de sécuriser les applications Lovable et Supabase.
Me contacter