Sécurité Supabase · version 1doc vérifiée le 17 octobre 2026par Vincent le Serpent
La checklist de sécurité Supabase
Supabase est génial pour sortir vite une première app. Mais la même facilité te laisse livrer une base ouverte sans t'en rendre compte : mon premier SaaS s'est fait hacker comme ça. Voici tout ce que je vérifie avant de mettre un site en prod.
Les huit réflexes, en un coup d'œil
- 1La RLS est activée sur chaque table
- 2Personne ne peut se mettre is_admin = true
- 3La clé secret ne sort jamais du serveur
- 4Les fichiers privés sont dans un bucket privé
- 5Les fonctions SECURITY DEFINER sont sous contrôle
- 6L'authentification est réglée pour la prod
- 7Rien de secret ne part dans le navigateur
- 8Tu as attaqué ton propre site avant de livrer
Écrit depuis la doc officielle Supabase et Next.js, vérifiée le 17 octobre 2026. Aucun email à donner pour lire cette page.
La RLS, activée partout
C'est la seule barrière entre un inconnu et tes données. Sans elle, ta table est ouverte.
Supabase te donne une API automatique sur ta base (une Postgres, gratuite pour démarrer) : dès que tu crées une table, elle est requêtable depuis Internet avec la clé publique du projet. Ce qui décide qui lit quoi, ce n'est pas la clé : c'est la Row Level Security (RLS), des règles posées sur chaque table. Une table sans RLS, dans un schéma exposé, est lisible et modifiable par n'importe quel visiteur.
- Active la RLS sur chaque table du schéma
public:alter table public.ma_table enable row level security; - Passe le Security Advisor du dashboard (ou
supabase db advisors) et règle chaque ERREUR. Il liste les tables « unrestricted », les policies posées sur une table dont la RLS est désactivée (elles ne s'appliquent pas), et les colonnes sensibles exposées. - RLS activée mais aucune policy = tout est refusé. C'est le bon défaut (« deny by default ») : tu ouvres ensuite, ligne par ligne, avec des policies.
- Une policy par opération (
select,insert,update,delete), filtrée par(select auth.uid()); jamais deusing (true)sur des données privées. - Attention aux vues : par défaut une vue ignore la RLS des tables qu'elle lit. Sur Postgres 15+, crée-la en
security_invoker = true, sinon ne l'expose pas.
SourceSupabase — Row Level Security · Database Advisors · Going into prod
Le piège de la colonne is_admin
La RLS dit quelles lignes on peut modifier, pas quelles colonnes. C'est là que la plupart des SaaS se font avoir.
Voici le trou le plus courant, et le plus discret. Tu écris une policy saine : « un utilisateur peut modifier sa ligne de profil ».
create policy "je modifie mon profil"
on public.profiles for update
using ( (select auth.uid()) = id )
with check ( (select auth.uid()) = id );
Le problème : cette règle dit quelle ligne, pas quelles colonnes. Depuis la console du navigateur, avec la clé publique, l'utilisateur envoie :
supabase.from('profiles')
.update({ is_admin: true })
.eq('id', monPropreId)
La policy est satisfaite — c'est bien sa ligne — et il devient admin. Cacher le champ dans ton interface ne change rien : l'API répond directement. Quatre façons de fermer le trou :
| Approche | Ce que tu fais |
|---|---|
| Table à part (recommandé par la doc) | Sors role dans une table user_roles que le client peut lire mais jamais écrire ; seul le serveur y touche. |
| Droits par colonne | revoke update sur la table, puis grant update (display_name, avatar_url) aux seules colonnes permises. |
Vérrou dans le with check | Impose que la nouvelle valeur sensible soit égale à l'ancienne. Ça marche, mais c'est fragile. |
Trigger before update | Refuse la requête si is_admin change et que l'appelant n'est pas le serveur. |
SourceSupabase — Column Level Security · Database linter (lint 0015, references_user_metadata)
Les deux clés : publique, et secrète
L'une est faite pour être dans ton site web. L'autre, si elle fuite, ouvre toute ta base.
Supabase te donne deux clés (renommées récemment, les anciens noms cohabitent encore) :
| Clé | Où elle va |
|---|---|
publishable (ex-anon) | Publique par conception. Elle part dans ton bundle, c'est normal : elle passe par la RLS, donc c'est la RLS qui protège. Elle identifie le projet, ce n'est pas un secret. |
secret (ex-service_role) | Contourne toute la RLS. Uniquement côté serveur (Edge Function, API, job). Jamais dans le front, jamais dans git, jamais dans une variable préfixée NEXT_PUBLIC_. |
- Cherche la clé secrète là où elle n'a rien à faire : dans le bundle (
grep sb_secret_sur ton build), dans les.envcommités, et dans l'historique git (git log -p -S sb_secret_). - Si elle a fuité une seule fois, considère-la compromise : corrige la cause, puis fais tourner la clé (crée la neuve, déploie, supprime l'ancienne). La retirer du code ne suffit pas : les anciens builds et les caches la gardent.
- Ne compte pas sur le blocage navigateur. Supabase refuse une clé secrète envoyée depuis un navigateur (erreur 401), mais c'est un garde-fou : une clé fuitée reste utilisable depuis n'importe quel autre outil.
Le Storage : tes fichiers privés le restent
C'est le point qui fait fuiter les formations payantes : la vidéo s'affiche, donc le navigateur a reçu son URL.
Si tu vends du contenu — des vidéos, des PDF — le piège est simple : dès que le navigateur affiche le fichier, il en a reçu l'URL. Les outils de développement, l'onglet réseau ou le code source la récupèrent. Un « paywall » qui ne fait que masquer le lecteur ne protège rien.
- Les fichiers payants vont dans un bucket privé. Les buckets sont privés par défaut ; dans un bucket public, la lecture est ouverte à quiconque a l'URL.
- On les sert par URL signée (
createSignedUrl), générée côté serveur, après avoir vérifié que la personne a payé, et qui expire vite (quelques minutes). - Ne stocke jamais l'URL publique du fichier en base : une table qui liste tes vidéos avec leur lien, lisible par la clé publique, fuite tout le catalogue d'un coup.
- Pose des policies RLS sur
storage.objects, en liant le chemin du fichier au droit de la personne (pas un simple « connecté »), et vérifie qu'un bucket public n'a pas de policy qui laisse lister son contenu.
SourceSupabase — Storage buckets · Storage access control · Signed URLs
Les fonctions SECURITY DEFINER
Une fonction mal réglée rend ouvert ce que ta RLS protège. Elle est appelable depuis l'API.
Une fonction Postgres en SECURITY DEFINER s'exécute avec les droits de son créateur — souvent un rôle qui contourne la RLS. Comme toute fonction est, par défaut, exécutable par n'importe quel rôle (anon compris) via /rest/v1/rpc, elle peut renvoyer des lignes que l'appelant ne devrait pas voir.
- Préfère
SECURITY INVOKER(le défaut) ; chaqueDEFINERdoit être justifié. - Toute fonction
DEFINERfixeset search_path = ''et qualifie les noms (public.ma_table) — sinon, détournement possible (lint0011). - Révoque
EXECUTEsur les fonctions qui n'ont pas à être publiques :revoke execute on function … from public, anon; - Une fonction qui renvoie des données par utilisateur revérifie la propriété dans son corps (
where user_id = (select auth.uid())) : le droit d'exécuter ne limite pas les lignes rendues.
L'authentification, réglée pour la prod
Les réglages par défaut sont faits pour le développement. Avant de livrer, tu repasses dessus.
- Confirmation d'email activée en production (sur un projet auto-hébergé, elle est désactivée par défaut).
- Site URL = ton URL de production, et une liste blanche de Redirect URLs en chemins exacts — pas de joker
**large, réservé aux previews. - Mot de passe : longueur minimale raisonnable, et protection contre les mots de passe déjà fuités (HaveIBeenPwned, à partir du plan Pro).
- Limites de débit et CAPTCHA sur inscription, connexion et réinitialisation ; SMTP sur ton propre domaine.
- Ne publie pas les emails : le schéma
authn'est pas exposé à l'API ; passe par une tableprofilessous RLS, et n'y mets que du public. - Le JWT est signé mais lisible : n'y mets jamais de secret. Côté serveur, vérifie le jeton auprès de Supabase, ne te fie pas à un décodage local.
- Connexions anonymes activées ? Un utilisateur anonyme porte le rôle
authenticated: relis toutes tes policiesto authenticated.
SourceSupabase — Going into prod · Redirect URLs · JWTs
Côté Next.js : rien de secret ne part au navigateur
La moitié des fuites ne viennent pas de Supabase, mais de ce que ton front compile et envoie.
Supabase se branche presque toujours à une appli — souvent Next.js, hébergée sur Vercel dans ma stack. Deux pièges s'ajoutent à la base de données.
- Tout ce qui est préfixé
NEXT_PUBLIC_est public : Next.js l'écrit en dur dans le JavaScript du navigateur au build.NEXT_PUBLIC_SUPABASE_URLet la clé publishable, d'accord ; une clé secret préfixée ainsi, catastrophe. (Même piège avec le blocenv: {}denext.config.js.) - Une page compilée en statique est publique, données comprises. Ce qu'un composant serveur lit au build est figé dans le HTML et servi à tous ; avec
output: 'export', il n'y a aucun serveur, donc rien n'est protégeable par ce biais. Ne lis jamais de donnée privée dans une page destinée à être statique. - Le contrôle d'accès se fait côté serveur, au plus près de la donnée. Une route d'API, une Server Action : chacune revérifie qui appelle et si la ressource lui appartient (sinon, on change l'
iddans l'URL et on lit la ressource d'un autre). Cacher un bouton ne protège pas la donnée derrière. - Les source maps de prod sont désactivées (défaut),
/publicne contient aucun fichier réservé, et Next.js est à jour (la faille de contournement du middleware, CVE-2025-29927, concernait les apps auto-hébergées).
SourceNext.js — Environment variables · Data security · Authentication
Teste ton propre site : demande à Claude de l'attaquer
La meilleure façon de trouver un trou avant un inconnu, c'est de chercher comme lui — sur ta propre app.
Une IA de code est très douée pour te dire que c'est sécurisé, même quand ça ne l'est pas. La parade : lui demander l'inverse, de chercher ce qui est exposé, sur ta propre application. C'est un auto-audit, la même idée que de relire son code avant de livrer. Colle ces prompts dans ton assistant, avec ton dépôt ouvert.
D'abord le cadre : on ne teste que sa propre app, ou une app pour laquelle on a une autorisation écrite. Jamais le site de quelqu'un d'autre. Tout ce qui écrit se fait sur un environnement de test, avec des comptes que tu as créés.
1. Lister ce qui n'a pas de RLS
Contexte : je suis le propriétaire et le seul développeur de cette app (Next.js + Supabase), j'audite MON PROPRE projet avant la prod. Tu as mon dépôt et mes migrations SQL.
En lecture seule :
1. Liste chaque table du schéma public avec : RLS activée oui/non, nombre de policies.
2. Pour chaque table SANS RLS, dis ce qu'un porteur de la clé publique peut lire et écrire.
3. Pour chaque policy, dis ce qu'un utilisateur lambda pourrait lire ou modifier qu'il ne devrait pas (cherche : using (true), absence de with check, policies basées sur user_metadata, vues sans security_invoker, fonctions SECURITY DEFINER exposées).
Rends un tableau : table | problème | gravité | correctif SQL. Ne modifie rien, et ne marque "OK" que ce que tu as vérifié.2. Ce que donne ta clé publique, vue de l'extérieur
Contexte : ceci est MON projet Supabase, j'en suis propriétaire et j'autorise ce test.
URL du projet : <la tienne> — clé publique (déjà dans mon bundle) : <la tienne>.
Mets-toi à la place d'un visiteur anonyme qui n'a que cette clé. En LECTURE seule, limit=3, jamais d'écriture :
1. Quelles tables et RPC l'API expose-t-elle ?
2. Pour chacune, un GET renvoie-t-il des lignes ?
3. Signale toute donnée sensible visible (emails, tokens, clés, URLs de fichiers privés) — masque les valeurs dans ton rapport.
4. Quels buckets Storage sont publics et listables ?
Une réponse saine = "0 ligne" ou "permission denied" partout, sauf données volontairement publiques.3. Tenter l'escalade de privilèges (sur un compte de test)
Contexte : MON app, environnement de test, avec un compte user_a que j'ai créé. J'autorise ce test sur mon propre projet.
1. Lis le schéma de ma table profils (role, is_admin, plan…) et ses policies update/insert.
2. Avec le jeton de user_a, tente via l'API de passer is_admin / role / plan à une valeur élevée sur sa propre ligne, puis sur la ligne d'un autre.
3. Dis-moi, par champ : protégé ou modifiable, la cause dans la policy, le correctif.
Remets chaque valeur d'origine après l'essai. Rien sur la prod ni sur des comptes réels. Un champ "modifiable" = échec bloquant.4. Chercher un secret ou une URL privée dans le bundle
Contexte : MON app Next.js, que je m'apprête à déployer.
1. Fais un build local, puis cherche dans .next/static et le HTML : clé service_role / secret, JWT, clés d'API tierces (sk_…), chaînes de connexion Postgres.
2. Liste les variables NEXT_PUBLIC_* et dis lesquelles ne devraient pas être publiques.
3. Cherche des URLs de contenu censé être privé (liens directs vers un bucket, une vidéo, un PDF ; URLs signées sans expiration ; fichiers payants dans /public).
Masque les secrets trouvés (4 premiers caractères). Dis-moi lesquels je dois considérer comme compromis et faire tourner.5. Vérifier que le contenu payant est vraiment fermé
Contexte : MON site vend des vidéos. J'autorise ce test sur mon propre site, avec un compte gratuit que j'ai créé.
1. Lis le code du lecteur et de l'API qui sert les médias : l'accès est-il décidé côté serveur, ou seulement dans l'interface ?
2. Récupère l'URL d'une vidéo payante (réseau ou code), puis demande-la sans compte et avec un compte gratuit. Réponse attendue : 401/403.
3. Si les URLs sont signées : quelle expiration ? Une URL expirée est-elle réutilisable ?
4. Le bucket est-il public ? Les noms sont-ils devinables ? Peut-on lister le dossier ?
Preuve minimale (le code HTTP suffit), ne télécharge pas les fichiers.Et un dernier, en relecteur sceptique : « Voici les rapports précédents de mon app. Dédoublonne, classe par gravité, et pour chaque point critique dis-moi comment je le reproduis moi-même. Signale surtout ce que tu n'as PAS pu vérifier, plutôt que de conclure que tout est bon. » La liste des angles morts compte autant que les failles trouvées.
SourceAnthropic — Automate security reviews with Claude Code · OWASP — Authorization Cheat Sheet
Les autres ressources
Gratuites, en français, sans email à donner.
- Un agent IA 100 % managé sur Google CloudLes quatre briques d'un agent managé chez Google, en schémas : Agent Runtime, Agent Search et tes données Gmail, Drive, Calendar, Model Armor sur Agent Gateway contre le mail piégé.
- Ma stack qui ne coûte rienComment je range et héberge une dizaine de sites pour presque rien. Quel serveur choisir, les moins chers, et les 7 hébergeurs gratuits de 2026.
- Les bases de données SQL gratuitesNeon, Supabase, PostgreSQL, SQLite : les quatre bases que je conseille pour démarrer sans payer, ce que chaque offre gratuite donne vraiment, et laquelle prendre.
- Terraform et l'infrastructure as codeLe fichier complet, les quatre commandes, le state, la même config sur trois environnements, et pourquoi l'IA se trompe sur le Terraform d'entreprise.