Auditez la sécurité au niveau des lignes sur n'importe quel Postgres
Quinze contrôles pour les défaillances qui font fuiter une base Postgres dans la vraie vie — pas pour celles qui sont faciles à vérifier. Fonctionne sur Supabase, Neon, RDS, Cloud SQL, ou un conteneur sur votre portable. Une commande, rien à installer, aucune inscription.
- Read-only
- Des SELECT sur les catalogues système et rien d'autre. Il ne lit jamais vos données.
- Rien ne quitte la machine
- Aucune télémétrie, aucun envoi, aucun réseau au-delà de la chaîne de connexion que vous lui passez.
- Rebase n'est pas requis
- Il lit la base de données, pas votre code. Il n'a besoin d'aucun Rebase et n'en demande aucun.
Les quinze contrôles
Chaque constat imprimé par l'outil porte l'identifiant ci-dessous, la raison pour laquelle il compte, et le SQL qui le corrige. La sévérité est le pire niveau qu'un contrôle peut rapporter — plusieurs se rétrogradent selon ce qu'ils trouvent.
Table exposed without row-level security
rls-disabledA table with RLS disabled that also grants SELECT/INSERT/UPDATE/DELETE to a role an unauthenticated or untrusted caller can reach.
Comment corriger →Policy grants unconditional access
policy-always-trueA permissive policy whose USING or WITH CHECK expression is always true.
Comment corriger →Policy only checks that a caller id exists
policy-anonymous-tautologyA policy whose expression is `auth.uid() IS NOT NULL`-shaped: it separates signed-in from signed-out callers but scopes no rows.
Comment corriger →View reads past its base table's RLS
view-bypasses-rlsA view granted to an untrusted role that selects from an RLS-protected table and runs with its owner's privileges instead of the caller's.
Comment corriger →Materialized view exposes RLS-protected data
matview-bypasses-rlsA materialized view granted to an untrusted role whose defining query reads a table with row-level security enabled.
Comment corriger →Unauthenticated callers can write
anonymous-write-allowedA permissive INSERT/UPDATE/DELETE policy reachable without authentication whose check expression accepts any row, backed by a matching grant.
Comment corriger →Policy admits every signed-in caller to every row
policy-authenticated-tautologyA policy whose expression is only "the caller is signed in and not anonymous": it excludes signed-out callers correctly and scopes no rows between accounts.
Comment corriger →Unqualified column inside a policy subquery
unqualified-column-in-subqueryA bare column name in an EXISTS/IN subquery that exists on both the inner relation and the policy's own table, so Postgres binds it to the inner one.
Comment corriger →Many-to-many join table without RLS
junction-table-unprotectedA table that is essentially just two foreign keys, both pointing at RLS-protected tables, that has no row-level security of its own.
Comment corriger →RLS enabled but not forced for the table owner
rls-enabled-not-forcedA table with RLS enabled where the owning role is exempt from its own policies.
Comment corriger →RLS enabled with no policies (denies everything)
rls-enabled-no-policiesA table with row-level security enabled but not a single policy defined.
Comment corriger →Policies target roles nothing connects as
policy-role-unreachableEvery policy on a table names roles that do not exist, cannot log in, and that no login role inherits — so no policy ever applies and the table reads as empty.
Comment corriger →Table privileges granted to PUBLIC
grant-to-publicA SELECT/INSERT/UPDATE/DELETE privilege granted to PUBLIC on a table.
Comment corriger →SECURITY DEFINER routine with a mutable search_path
security-definer-mutable-search-pathA SECURITY DEFINER function or procedure that does not pin search_path, so the caller controls how its identifiers resolve.
Comment corriger →Policy calls current_setting() without missing_ok
current-setting-throwsA policy expression calling current_setting('x') with one argument, which raises rather than returning NULL when the setting is unset.
Comment corriger →À quoi ressemble une exécution
Chaque constat nomme la relation, dit qui peut l'atteindre et ce qu'il obtiendrait, et affiche le SQL qui la referme. Il sort avec un code non nul au-delà d'une sévérité que vous choisissez : sa place est dans la CI.
$ npx @rebasepro/rls-check --role app_reader rls-check 0.25.1 · read-only Row-Level Security audit ──────────────────────────────────────────────────────────────────────────────────────── Database db.acme.internal:5432/production Server PostgreSQL 16.4 Platform PostgreSQL (no platform markers) Exposed PUBLIC, app_reader (add yours with --role) Scanned 1 schema · 3 tables · 2 policies · 15 checks Note This scan connected as a role that row-level security cannot constrain — a superuser, a table owner, or a role with BYPASSRLS. That is why it can read the true catalog, and it is also why nothing below describes what this connection experiences. The findings are about what OTHER roles get. CRITICAL · 3 findings ──────────────────────────────────────────────────────────────────────────────────────── [critical] view-bypasses-rls public.billing_overview View public.billing_overview reads public.ledger without security_invoker and is readable by app_reader The view is owned by postgres and `security_invoker` is not set, so its query executes with postgres's privileges rather than the caller's. Row-level security on public.ledger is evaluated for postgres, not for the role that selected from the view. Impact If this view is reachable over an API as app_reader, a caller reads rows from public.ledger that the policies on that table were written to withhold — the view is an unfiltered path around them. Fix ALTER VIEW "public"."billing_overview" SET (security_invoker = true); -- Callers then need their own SELECT privilege on public.ledger, and the -- policies there apply to them. Docs https://rebase.pro/docs/rls-check#view-bypasses-rls [critical] policy-always-true public.documents · policy "documents_read" Policy "documents_read" on public.documents is USING (true) for app_reader This permissive SELECT policy's USING expression is a constant truth, so it matches every row for app_reader. Permissive policies are ORed together, so this one alone satisfies the table's row filter no matter how strict the others are. Impact If this table is reachable over an API as app_reader, a caller can read every row — the policy applies no scoping whatsoever. Fix -- Replace the constant with the scoping you intended, e.g.: ALTER POLICY "documents_read" ON "public"."documents" USING (user_id = auth.uid()); -- or, if unconditional access really is intended, drop the policy and say so -- with an explicit grant instead: -- DROP POLICY "documents_read" ON "public"."documents"; Docs https://rebase.pro/docs/rls-check#policy-always-true [critical] rls-disabled public.invoices public.invoices has row-level security disabled and is granted to app_reader Row-level security is not enabled on this table, so Postgres applies no per-row filter at all — policies, if any exist, are never consulted. app_reader holds DELETE, INSERT, SELECT and UPDATE on it. Impact If this table is reachable over an API that connects as app_reader, a caller can read every row and delete, insert and update any row, with no tenant or owner scoping. Fix ALTER TABLE "public"."invoices" ENABLE ROW LEVEL SECURITY; -- Enabling RLS with no policies denies every row to everyone but the owner, -- so add the policy you intend in the same migration, for example: -- CREATE POLICY "invoices_owner_select" ON "public"."invoices" -- FOR SELECT TO "app_reader" USING (user_id = auth.uid()); Docs https://rebase.pro/docs/rls-check#rls-disabled ──────────────────────────────────────────────────────────────────────────────────────── Summary critical 3 high 0 medium 0 low 0 info 0 3 confirmed · 0 worth checking · 15 checks run against 3 tables in 1 schema 1 of 3 tables have row-level security disabled Exit code 1 — at least one finding is "high" or worse (--fail-on high). Scanned 2026-09-05T09:14:02.881Z · read-only, and nothing left this machine. rls-check is free and maintained by the team behind Rebase — https://rebase.pro
Questions about rls-check
Peut-on l'exécuter en toute sécurité sur une base de production ?
Il fonctionne en lecture seule stricte. Il exécute uniquement des SELECT sur les catalogues système — pg_class, pg_policies, pg_proc, information_schema — et rien d'autre. Il ne lit jamais vos données, n'écrit rien, ne modifie aucun paramètre et n'effectue aucune requête réseau en dehors de la chaîne de connexion fournie. Aucune télémétrie, aucun envoi de données.
Faut-il utiliser Rebase ?
Non. Il ignore tout de votre framework et n'a aucun besoin de votre codebase — il lit directement la base de données. Il fonctionne avec Supabase, Neon, RDS, Cloud SQL, un conteneur sur votre machine ou tout ce qui supporte le protocole réseau Postgres. Il n'a aucunement besoin de Rebase.
Avec quel rôle doit-il se connecter ?
Un rôle capable de lire les catalogues — généralement un superuser, le propriétaire de la table ou un rôle avec BYPASSRLS. C'est délibéré et le rapport l'indique d'emblée : le scan lit le catalogue réel précisément parce que le RLS ne peut pas le restreindre, afin que les résultats décrivent ce que subissent les autres rôles, et non le comportement de cette connexion.
En quoi est-ce différent du linter de Supabase ?
Il y a un réel recoupement sur les cas évidents, et exécuter les deux est utile. Les vérifications proposées ici qui n'apparaissent généralement pas ailleurs concernent les cas indirects : une vue ou vue matérialisée qui contourne le RLS de sa table sous-jacente car elle s'exécute avec les droits de son propriétaire, une table de jointure many-to-many laissée sans protection entre deux entités protégées, ou une colonne non préfixée dans une sous-requête EXISTS que Postgres lie à la table interne — transformant un filtre de tenant en tautologie tout en semblant parfaitement correcte.
Peut-on l'exécuter dans la CI ?
Oui — c'est même son cas d'usage principal. Il renvoie un code de sortie non nul si des anomalies atteignent ou dépassent le seuil choisi, génère du JSON pour l'automatisation et permet d'ignorer des vérifications spécifiques par id pour qu'une exception délibérée ne fasse pas échouer chaque build. Les id constituent une API publique stable précisément pour cette raison.
Pourquoi une entreprise de backend offre-t-elle un scanner de sécurité ?
Parce que la thèse de Rebase est que l'autorisation a sa place dans la base de données plutôt que dans la couche située en amont, et qu'un outil qui audite le RLS Postgres concrétise cet argument sous une forme exécutable sur vos propres données plutôt qu'à travers un simple texte. Il est réellement utile, que vous consultiez ou non le reste de ce site — et s'il ne trouve rien, c'est une bonne après-midi et vous ne nous devez rien.
Puis corrigez-le une fois, dans le code.
Un scanner vous dit où les policies sont fausses. Rebase les compile depuis vos définitions de collection, si bien que la suivante est relue dans une pull request avant même d'atteindre une base.