Outil gratuit · sans compte

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.

~npx @rebasepro/rls-check
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.

Critical6 checks

Table exposed without row-level security

rls-disabled

A 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-true

A permissive policy whose USING or WITH CHECK expression is always true.

Comment corriger →

Policy only checks that a caller id exists

policy-anonymous-tautology

A 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-rls

A 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-rls

A 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-allowed

A permissive INSERT/UPDATE/DELETE policy reachable without authentication whose check expression accepts any row, backed by a matching grant.

Comment corriger →
High3 checks

Policy admits every signed-in caller to every row

policy-authenticated-tautology

A 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-subquery

A 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-unprotected

A 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 →
Medium6 checks

RLS enabled but not forced for the table owner

rls-enabled-not-forced

A 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-policies

A table with row-level security enabled but not a single policy defined.

Comment corriger →

Policies target roles nothing connects as

policy-role-unreachable

Every 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-public

A 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-path

A 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-throws

A 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.

rls-check · production
$ 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

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.

~pnpm dlx @rebasepro/cli init