15 août 2026

RLS Supabase : le guide pour ne pas se faire vider sa base

Row Level Security (RLS) est la seule ligne de défense entre n'importe quel visiteur de ton site et le contenu brut de ta base Supabase. La clé anon est publique par design — elle est dans ton bundle JS, tout le monde peut l'utiliser depuis la console du navigateur pour parler directement à ta base. Sans RLS bien configuré, ta base de données est aussi ouverte que si tu avais publié ses identifiants.

Ce que RLS fait réellement

Par défaut, une table Postgres sans RLS activé est interrogeable par n'importe quel rôle qui a un GRANT dessus — et Supabase donne un GRANT large au rôle anon sur le schéma public par défaut. RLS ajoute une condition invisible, évaluée par Postgres à chaque requête, qui filtre les lignes qu'un rôle a le droit de voir ou modifier :

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

CREATE POLICY "users_read_own_profile" ON profiles
  FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

Sans cette policy (ou avec RLS désactivé), une requête SELECT * FROM profiles faite avec la clé anon renvoie toutes les lignes de tous les utilisateurs, pas juste celles de la personne connectée.

Le piège n°1 : RLS activé, policy inexistante

Un scaffold IA active souvent RLS par bon réflexe — c'est devenu une pratique connue — mais sans forcément créer les policies qui vont avec. Résultat contre-intuitif : RLS activé sans aucune policy SELECT ne rend pas la table plus ouverte, il la ferme complètement, y compris à l'app elle-même. C'est souvent découvert vite (l'app ne fonctionne plus), donc ce n'est pas le scénario le plus dangereux.

Le vrai piège, c'est l'inverse : une policy qui existe, mais trop permissive :

-- Ça ressemble à une policy de sécurité. Ça n'en est pas une.
CREATE POLICY "allow_all" ON orders
  FOR SELECT
  USING (true);

USING (true) veut dire "toujours vrai, pour toutes les lignes" — c'est l'équivalent RLS de ne rien avoir. Un IA qui génère un CRUD complet en une passe met souvent ça "pour que ça marche", sans revenir la resserrer.

Le piège n°2 : policies d'écriture inutiles

C'est exactement la faille qu'on a trouvée sur notre propre app (détaillée ici) : si toutes tes écritures réelles passent par le serveur avec un client service_role (qui contourne RLS entièrement), alors une policy INSERT ou UPDATE ouverte à anon ne sert à rien côté légitime — mais reste une porte grande ouverte pour n'importe qui d'autre.

Le piège n°3 : les policies sur d'autres tables via subquery

Un cas plus subtil : une policy qui vérifie une condition via une sous-requête sur une autre table.

CREATE POLICY "read_own_findings" ON findings
  FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM scans
      WHERE scans.id = findings.scan_id AND scans.paid = true
    )
  );

Cette policy ne fonctionne que si le rôle qui exécute la requête a aussi le droit de SELECT sur scans. Si tu as révoqué l'accès direct à scans pour anon (par exemple pour cacher certaines colonnes), la sous-requête échoue silencieusement et la policy se comporte comme si elle était toujours fausse — cassant l'accès légitime plutôt que de fuiter, mais un bug de production difficile à diagnostiquer si tu ne t'y attends pas.

Checklist RLS avant de mettre en prod

  • RLS est-il activé sur toutes les tables du schéma public, y compris celles créées récemment ? (Le dashboard Supabase liste les tables sans RLS avec un avertissement.)
  • Est-ce qu'une policy quelconque contient USING (true) ou WITH CHECK (true) sans que ce soit un choix délibéré (table volontairement publique) ?
  • Les écritures qui comptent (paiement, rôle, statut admin) passent-elles par un client service_role côté serveur ? Si oui, les policies d'écriture ouvertes à anon sont-elles vraiment nécessaires ?
  • As-tu testé une requête réelle avec juste la clé anon (comme le ferait un attaquant), pas seulement vérifié que l'app fonctionne connectée ?

Ce dernier point est celui que la plupart des devs sautent — c'est justement ce que le scan Vetora automatise : il extrait les vrais noms de table appelés par ton JS et les interroge avec la clé anon publique, pour voir ce qui répond vraiment.