Audit row-level security on any Postgres
Fifteen checks for the failures that make a Postgres database leak in practice — not the ones that are easy to check for. Works on Supabase, Neon, RDS, Cloud SQL, or a container on your laptop. One command, nothing to install, nothing to sign up for.
- Read-only
- SELECTs against the system catalogues and nothing else. It never reads your data.
- Nothing leaves the machine
- No telemetry, no upload, no network beyond the connection string you pass it.
- No Rebase required
- It reads the database, not your codebase. It needs no Rebase and asks about none.
The fifteen checks
Each finding the tool prints carries the id below, the reason it matters, and the SQL that fixes it. Severity is the worst level a check can report — several grade themselves down depending on what they find.
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.
How to fix it →Policy grants unconditional access
policy-always-trueA permissive policy whose USING or WITH CHECK expression is always true.
How to fix it →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.
How to fix it →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.
How to fix it →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.
How to fix it →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.
How to fix it →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.
How to fix it →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.
How to fix it →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.
How to fix it →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.
How to fix it →RLS enabled with no policies (denies everything)
rls-enabled-no-policiesA table with row-level security enabled but not a single policy defined.
How to fix it →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.
How to fix it →Table privileges granted to PUBLIC
grant-to-publicA SELECT/INSERT/UPDATE/DELETE privilege granted to PUBLIC on a table.
How to fix it →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.
How to fix it →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.
How to fix it →What a run looks like
Every finding names the relation, says who can reach it and what they would get, and prints the SQL that closes it. Exits non-zero above a severity you choose, so it belongs in CI.
$ npx @rebasepro/rls-check --role app_reader rls-check 0.19.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
Then fix it once, in code.
A scanner tells you where the policies are wrong. Rebase compiles them from your collection definitions, so the next one is reviewed in a pull request before it ever reaches a database.
