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.24.0 · 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
Is it safe to run against a production database?
It is read-only, and narrowly so. It issues SELECTs against the system catalogues — pg_class, pg_policies, pg_proc, information_schema — and nothing else. It never reads your data, writes nothing, changes no setting, and makes no network request beyond the connection string you give it. There is no telemetry and no upload.
Do I need to be using Rebase?
No. It knows nothing about your framework and asks nothing of your codebase — it reads the database. It works against Supabase, Neon, RDS, Cloud SQL, a container on your laptop, or anything else that speaks the Postgres wire protocol. It needs no Rebase and asks about none.
What does it need to connect as?
A role that can read the catalogues — usually a superuser, the table owner, or something with BYPASSRLS. That is deliberate and the report says so at the top: the scan reads the true catalogue precisely because row-level security cannot constrain it, so the findings describe what other roles experience, not what this connection does.
How is this different from Supabase's own linter?
There is real overlap on the obvious cases, and both are worth running. The checks here that tend not to appear elsewhere are the indirect ones: a view or materialized view reading past its base table's RLS because it runs as its owner, a many-to-many join table left unprotected between two protected endpoints, and a bare column inside an EXISTS subquery that Postgres binds to the inner table — which turns a tenant filter into a tautology while looking completely correct.
Can I run it in CI?
Yes — that is the intended home for it. It exits non-zero when findings at or above a threshold you choose are present, emits JSON for machine consumption, and supports skipping individual checks by id so a deliberate exception does not fail every build. The ids are a stable public API for exactly this reason.
Why is a backend company giving away a security scanner?
Because the argument Rebase makes is that authorization belongs in the database rather than in the layer in front of it, and a tool that audits Postgres RLS is that argument in a form you can run against your own data instead of read. It is genuinely useful whether or not you ever look at the rest of this site — and if it finds nothing, that is a good afternoon and you owe us nothing.
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.