Sur la plupart des SaaS Supabase que je teste, j'arrive à lire la base clients en quelques minutes. Pas en forçant quoi que ce soit, juste parce que la RLS (Row Level Security) est mal réglée. Et le pire, c'est que ces erreurs passent la relecture de code sans problème. On ne les voit qu'en testant. Voici les plus fréquentes, et comment les corriger avant qu'un attaquant les trouve.

La RLS, c'est quoi, et pourquoi c'est ta seule barrière

Avec Supabase, ton front parle directement à la base via une API, avec une clé publique (la clé anon). Autrement dit, n'importe qui peut envoyer des requêtes à ta base depuis son navigateur. La seule chose qui empêche un visiteur de lire les données des autres, c'est la RLS : des règles, au niveau de la base, qui disent qui a le droit de voir et de modifier quelle ligne.

Si la RLS est absente ou trop permissive sur une table, cette table est lisible par tout le monde. Il n'y a pas de deuxième filet. C'est pour ça que c'est le premier endroit que je regarde.

Les erreurs de RLS que je trouve le plus souvent

1. La RLS n'est pas activée du tout

Une table créée dans Supabase n'a pas la RLS activée par défaut. Si elle est dans un schéma exposé à l'API, elle est lisible par n'importe qui avec la clé anon. C'est l'erreur numéro un, et elle touche souvent une table qu'on a ajoutée vite fait, après coup.

-- à faire sur CHAQUE table exposée à l'API
alter table public.profiles enable row level security;

Vérifie après chaque migration, pas seulement au début. Une seule table oubliée suffit.

2. La RLS est activée, mais sans aucune policy

Activer la RLS sans écrire de policy bloque tout, silencieusement. Pas d'erreur, juste des résultats vides. Le problème, c'est ce que ça pousse à faire ensuite : comme "rien ne marche", on ajoute une policy large pour débloquer, et on oublie de la resserrer.

3. Une policy qui autorise tout le monde

C'est la fausse sécurité la plus courante. Une policy du type USING (true) sur une table de données utilisateur, ça revient à ne pas avoir de RLS du tout. Ta policy doit comparer l'utilisateur connecté au propriétaire de la ligne.

-- une vraie policy : chacun ne voit que ses lignes
create policy "profiles: owner read"
on public.profiles for select
using ( (select auth.uid()) = user_id );

Le (select auth.uid()) plutôt que auth.uid() nu aide aussi Postgres à optimiser la requête.

4. Une seule policy pour toutes les opérations

Une policy sur select ne protège pas insert, update ni delete. Chaque commande a besoin de sa propre règle. Et sur insert et update, il te faut un with check en plus du using, sinon un utilisateur peut modifier une ligne pour l'attribuer à quelqu'un d'autre.

create policy "profiles: owner update"
on public.profiles for update
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

5. Une fonction SECURITY DEFINER qui contourne la RLS

Les fonctions security definer s'exécutent avec les droits de leur créateur, et peuvent donc contourner la RLS sans que rien ne le signale. Si tu en utilises, fige leur search_path et refais le contrôle d'accès à l'intérieur de la fonction.

-- sinon, escalade de privilèges possible
alter function public.ma_fonction() set search_path = '';

6. La clé service_role côté client

La clé service_role ignore complètement la RLS et donne un accès total à la base. Elle ne doit jamais se retrouver dans ton front, ton bundle JavaScript, ou une variable exposée au navigateur. Si tu la trouves côté client, ce n'est pas du nettoyage, c'est un incident : régénère-la tout de suite.

7. Le Storage oublié

La RLS des tables ne protège pas tes fichiers. Un bucket rendu public "pour que le CDN marche", avec des chemins prévisibles, et les fichiers de tes clients deviennent accessibles à qui a l'URL. Tes buckets de données utilisateur doivent être privés, avec leurs propres policies sur storage.objects.

Le point commun de ces 7 erreurs : elles passent la relecture de code. Le SQL a l'air juste. On ne les voit qu'en testant avec de vrais comptes.

Comment tester ta RLS en dix minutes

La seule façon fiable de savoir si ta RLS tient, c'est de l'attaquer avec deux comptes :

  • Crée deux utilisateurs de test, A et B.
  • Connecté en tant que A, essaie de lire, modifier et supprimer les lignes de B, en changeant simplement les identifiants.
  • Lis toujours le corps de la réponse, pas seulement le code HTTP. Une table sans policy renvoie souvent 200 avec un tableau vide, ce qui est normal. La vraie faille, c'est 200 avec des lignes dedans.

Tu peux aussi simuler le rôle applicatif directement en SQL, sans passer par le front :

set local role authenticated;
select set_config('request.jwt.claims',
  '{"sub":"<uuid-utilisateur-B>","role":"authenticated"}', true);
-- puis tente de lire les lignes qui ne t'appartiennent pas
select * from public.profiles;

Attention : si tu testes en service_role, il n'y a pas de JWT, auth.uid() vaut NULL, et ton test ne reproduit pas la réalité. Teste toujours avec le rôle authenticated.

Pour aller plus loin

Si tu veux industrialiser, écris des tests pgTAP qui rejouent ces scénarios en intégration continue, pour qu'une migration ne puisse plus désactiver une RLS sans casser le build. Et vérifie aussi les vues (elles peuvent contourner la RLS si elles ne sont pas en security_invoker) et les relations imbriquées de PostgREST (?select=*,autre_table(*)).

Pas sûr que ta RLS tienne ?

Lance un scan gratuit : en une minute, tu vois ce qu'un attaquant voit de ta base Supabase de l'extérieur. Et si tu veux que je teste tout en profondeur, table par table, c'est l'audit.

Scanner mon SaaS, gratuit

Voir les offres d'audit

Questions fréquentes

La RLS Supabase est-elle suffisante pour sécuriser un SaaS ?

C'est ta barrière principale, mais elle ne suffit que si elle est activée sur chaque table, avec une policy par commande, et testée avec de vrais comptes. Il faut aussi protéger le Storage, les fonctions security definer et la clé service_role.

Activer la RLS suffit-il ?

Non. Activer la RLS sans policy bloque tout silencieusement, ce qui pousse souvent à ajouter une policy trop permissive qui annule la protection. Il faut une règle précise par table et par commande.

← Retour au blog