L'application semblait prête pour la production. Une interface utilisateur épurée, une intégration Stripe fonctionnelle, un parcours d'intégration soigné. Elle avait été présentée sur Product Hunt, avait attiré plus de 200 utilisateurs dès la première semaine et générait déjà du MRR. Le fondateur l'avait construite en 48 heures lors d'un week-end de hackathon en utilisant Next.js et Supabase, la pile technologique de choix pour tous ceux qui veulent livrer rapidement.
Huit minutes après le début du scan Penetrify, nous avons signalé une vulnérabilité Critique : l'enregistrement de profil de chaque utilisateur — nom, e-mail, métadonnées de compte — était lisible par tout autre utilisateur authentifié via l'API REST auto-générée de Supabase. Aucune sophistication d'attaquant requise. Il suffisait de remplacer votre ID utilisateur par un autre dans l'URL.
Voici l'histoire de comment cela s'est produit, pourquoi c'est plus courant que quiconque ne veut l'admettre, et à quoi ressemblait exactement la correction.
Ce qu'est la sécurité au niveau des lignes (Row Level Security) de Supabase — Et ce qui se passe quand elle est désactivée
Supabase est construit sur PostgreSQL et expose directement vos tables de base de données via une API REST (via PostgREST) et une bibliothèque cliente JavaScript. C'est une fonctionnalité véritablement puissante pour le développement rapide : vous pouvez interroger votre base de données depuis le frontend sans écrire une seule route d'API.
Le mécanisme qui empêche les utilisateurs de lire les données des autres s'appelle la sécurité au niveau des lignes (Row Level Security - RLS). Le RLS est une fonctionnalité de PostgreSQL qui vous permet de définir des politiques contrôlant quelles lignes un utilisateur de base de données donné peut SELECT, INSERT, UPDATE ou DELETE. Dans une application Supabase, vous écririez généralement une politique comme celle-ci :
-- Only allow users to read their own profile
CREATE POLICY "Users can view own profile"
ON profiles
FOR SELECT
USING (auth.uid() = user_id);
Lorsque le RLS est activé et qu'une politique comme celle-ci est en place, une requête pour le profil d'un autre utilisateur renvoie zéro ligne — la base de données applique le contrôle d'accès au niveau des lignes, avant que toute donnée n'atteigne l'application.
Lorsque le RLS est désactivé ou qu'aucune politique n'existe, la table se comporte comme si chaque ligne était publique pour toute requête authentifiée. L'API REST de Supabase, accessible à https://[project].supabase.co/rest/v1/profiles, renverra toutes les lignes de la table — les données de chaque utilisateur — à quiconque possède un JWT valide de ce projet.
Ce que nous avons trouvé : Cinq vulnérabilités, dont deux Critiques
Le scan a duré huit minutes en mode rapide sur l'application en production. Voici les vulnérabilités par ordre de gravité :
CRITIQUE — RLS Supabase non activé sur la table des profils
La table des profils avait le RLS désactivé. Tout utilisateur authentifié pouvait l'interroger via le point d'accès REST de Supabase et récupérer tous les enregistrements d'utilisateurs. La requête qui l'a démontré :
GET /rest/v1/profiles?select=*
Authorization: Bearer [any valid user JWT]
HTTP/1.1 200 OK
[
{"id": "uuid-1", "email": "user1@example.com", "full_name": "...", ...},
{"id": "uuid-2", "email": "user2@example.com", "full_name": "...", ...},
... (all 200+ user records)
]
En vertu du RGPD, il s'agit d'une exposition de données personnelles affectant chaque utilisateur enregistré. La clé anonyme de Supabase est intégrée dans le bundle JavaScript du frontend par conception — il est sûr de l'exposer publiquement lorsque le RLS est configuré correctement, car la clé seule ne donne aucun accès privilégié. Sans RLS, elle devient une clé maîtresse pour toutes les données utilisateur.
CRITIQUE — Vérification d'e-mail non appliquée sur les points d'accès protégés
Supabase permet les inscriptions par e-mail et mot de passe par défaut, et envoie un e-mail de confirmation pour vérifier l'adresse. Cependant, les routes d'API backend de l'application ne vérifiaient pas si l'utilisateur demandeur avait confirmé son e-mail avant d'accorder l'accès à des fonctionnalités protégées.
Un attaquant pouvait s'inscrire avec victim@example.com (l'adresse e-mail d'un utilisateur réel), ignorer complètement la vérification de l'e-mail et accéder immédiatement aux routes API protégées de l'application comme s'il était le propriétaire de cet e-mail. Combiné au problème RLS, cela signifiait qu'un attaquant non authentifié pouvait exfiltrer l'intégralité de la base de données des utilisateurs en créant un compte jetable avec n'importe quelle adresse e-mail.
MOYEN — IDOR sur /api/export
L'application disposait d'un point de terminaison d'exportation qui acceptait un ID utilisateur comme paramètre de requête :
GET /api/export?userId=abc123
Aucune vérification de propriété n'était effectuée côté serveur. Tout utilisateur authentifié pouvait exporter les données de n'importe quel autre utilisateur en substituant son propre ID par celui d'une cible. Les ID utilisateur étaient exposés dans les réponses API à travers toute l'application, rendant l'énumération triviale.
MOYEN — Pas de limitation de débit sur le point de terminaison de connexion
Le point de terminaison de connexion acceptait les requêtes d'authentification à environ 500 requêtes par seconde sans limitation ni verrouillage. Une attaque par bourrage d'identifiants (credential stuffing) contre la base d'utilisateurs de l'application ne rencontrerait aucune friction.
MOYEN — Jetons JWT stockés dans le localStorage sans rotation
Les JWT Supabase étaient stockés dans le localStorage, accessibles à tout JavaScript exécuté sur la page. Aucune rotation de jeton n'avait lieu lors des changements de privilèges. Une attaque XSS réussie sur n'importe quelle page donnerait à un attaquant une session persistante et valide.
Pourquoi cela se produit : Le piège des configurations par défaut de Supabase
Ce n'est pas l'histoire d'un développeur négligent. C'est l'histoire des configurations par défaut.
Lorsque vous créez une nouvelle table dans Supabase, RLS est désactivé par défaut. La propre documentation de Supabase le décrit clairement et recommande de l'activer, mais les exemples de démarrage rapide — ceux que les développeurs suivent réellement lorsqu'ils codent à 2h du matin pendant un hackathon — omettent souvent RLS par souci de concision. Vous voyez une requête fonctionnelle dans l'exemple, vous copiez le modèle, vous le déployez.
Le tableau de bord Supabase affiche une icône d'avertissement jaune sur les tables sans RLS. Il est facile de la manquer lorsque l'on est concentré sur l'interface utilisateur, l'intégration des paiements, le flux d'intégration. Il n'y a pas d'erreur, pas d'échec d'exécution, pas de symptôme évident. Tout fonctionne parfaitement. Les utilisateurs peuvent s'inscrire, se connecter et utiliser l'application. La vulnérabilité est complètement silencieuse.
Les failles de sécurité les plus dangereuses sont celles qui ressemblent exactement à un comportement correct.
Ce schéma apparaît dans presque toutes les applications basées sur Supabase que nous analysons et qui ont été conçues pour la vitesse. Non pas parce que les développeurs ne se soucient pas de la sécurité, mais parce que la pression du temps d'un hackathon ou d'un sprint de lancement ne laisse pas de place pour lire chaque section de la documentation.
La solution : Deux heures, aucune réécriture de code
Le fondateur a corrigé les cinq problèmes en moins de deux heures le soir même de la réception du rapport. Voici exactement ce qui a été fait :
RLS — 15 minutes dans le tableau de bord Supabase
L'activation de RLS et l'ajout de politiques appropriées n'ont nécessité aucune modification de code. Dans le tableau de bord Supabase, sous Éditeur de table → profils → Politiques RLS :
-- Activer RLS sur la table (un interrupteur dans le tableau de bord)
-- Ensuite, ajouter les politiques :
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = user_id);
-- Pour l'accès administrateur (si nécessaire) :
CREATE POLICY "Service role has full access"
ON profiles
USING (auth.role() = 'service_role');
Après avoir activé RLS et ajouté ces politiques, la requête en masse a renvoyé zéro ligne pour tout utilisateur authentifié effectuant une requête pour des données qui n'étaient pas les siennes.
Vérification de l'e-mail — une seule vérification de middleware
Les routes API de Next.js lisaient déjà l'objet utilisateur Supabase à partir du JWT à chaque requête. L'ajout de l'application de la vérification de l'e-mail était une vérification d'une seule ligne dans le middleware d'authentification :
const { data: { user } } = await supabase.auth.getUser()
if (!user?.email_confirmed_at) {
return res.status(403).json({ error: 'Email verification required' })
}
IDOR sur /api/export — correctif middleware d'une ligne
Le correctif a consisté à remplacer le paramètre userId fourni par l'utilisateur par le propre ID de l'utilisateur authentifié, extrait du JWT vérifié :
// Avant (vulnérable)
const userId = req.query.userId
// Après (corrigé)
const { data: { user } } = await supabase.auth.getUser()
const userId = user.id // toujours l'utilisateur authentifié — ne peut pas être usurpé
Limitation de débit — la limitation de débit intégrée de Vercel
L'application était hébergée sur Vercel. L'ajout d'une limitation de débit au point d'accès de connexion a nécessité l'ajout du package @upstash/ratelimit et l'encapsulation de la route — environ 20 lignes de code. Le fondateur a déployé ce correctif le lendemain matin.
L'exposition au GDPR
Le problème RLS a affecté plus de 200 utilisateurs enregistrés. Leurs enregistrements de profil complets — adresses e-mail, noms d'affichage et tout autre champ stocké dans la table des profils — étaient lisibles par tout autre utilisateur authentifié pendant toute la durée de vie de l'application.
En vertu de l'article 33 du GDPR, une violation de données personnelles qui est « susceptible d'engendrer un risque pour les droits et libertés des personnes physiques » doit être signalée à l'autorité de contrôle compétente dans les 72 heures suivant sa découverte. Le fait que cette violation particulière ait franchi ce seuil dépendrait de la sensibilité des données stockées et du nombre d'utilisateurs dont les données ont été réellement consultées — mais la fenêtre d'exposition était ouverte.
Le fondateur a renouvelé toutes les valeurs de configuration sensibles, activé le RLS et corrigé les problèmes restants le soir même. Aucune preuve d'accès non autorisé n'a été trouvée dans les journaux Supabase. Le problème a été maîtrisé avant de devenir un incident.
Comment vérifier votre propre application Supabase
Si vous utilisez une application basée sur Supabase, voici un auto-audit rapide que vous pouvez effectuer en cinq minutes :
- Ouvrez le tableau de bord Supabase → Éditeur de tables. Toute table sans icône de bouclier vert a le RLS désactivé. Si cette table contient des données utilisateur, elle est probablement exposée.
- Ouvrez les outils de développement de votre navigateur sur votre application de production → onglet Réseau → filtrez par l'URL de votre projet Supabase. Recherchez les requêtes vers
/rest/v1/[tablename]?select=*. Si vous voyez des requêtes renvoyant de nombreux enregistrements, vérifiez si elles devraient être limitées à l'utilisateur authentifié. - Vérifiez votre flux de vérification d'e-mail. Enregistrez un nouveau compte, ignorez l'e-mail de confirmation et tentez d'accéder directement aux parties protégées de votre application. Si vous pouvez accéder à des fonctionnalités protégées sans confirmer votre e-mail, votre backend n'applique pas la vérification.
- Vérifiez vos points d'accès d'exportation/téléchargement. Tout point d'accès qui accepte un ID utilisateur comme paramètre devrait le vérifier par rapport au JWT de l'utilisateur authentifié. Si le paramètre peut être librement substitué pour accéder aux données d'un autre utilisateur, il s'agit d'une IDOR.
Ces quatre vérifications prennent moins de dix minutes et couvrent les lacunes de sécurité Supabase les plus courantes que nous observons dans les applications de production.
Le schéma plus large
Cette étude de cas n'est pas inhabituelle. Elle est représentative de ce que nous trouvons dans la majorité des applications basées sur Supabase qui ont été construites lors d'un hackathon, d'un sprint de lancement rapide, ou par un fondateur solo sans expérience en sécurité.
La combinaison d'outils rapides, d'une excellente expérience développeur et de valeurs par défaut publiques qui privilégient la facilité d'utilisation à la sécurité signifie que de nombreuses applications sont livrées avec ces lacunes. Ces lacunes sont corrigeables — souvent en quelques minutes, nécessitant rarement plus qu'une politique SQL ou une vérification middleware d'une ligne. Le plus difficile est de savoir qu'elles existent.
L'équipe Supabase a réalisé un travail considérable pour rendre RLS plus visible dans le tableau de bord et la documentation. Mais l'écart entre « la documentation dit d'activer ceci » et « chaque application en production l'a réellement activé » reste important. Tant que les tests de sécurité automatisés ne feront pas partie de la liste de contrôle de lancement standard, ce schéma continuera d'apparaître.
