Audita la seguridad a nivel de fila en cualquier Postgres
Quince comprobaciones para los fallos que hacen que una base de datos Postgres filtre en la práctica — no para los que son fáciles de comprobar. Funciona en Supabase, Neon, RDS, Cloud SQL o un contenedor en tu portátil. Un comando, nada que instalar, nada a lo que registrarse.
- Read-only
- SELECTs contra los catálogos del sistema y nada más. Nunca lee tus datos.
- Nada sale de la máquina
- Sin telemetría, sin subidas, sin red más allá de la cadena de conexión que le pasas.
- No hace falta Rebase
- Lee la base de datos, no tu código. No necesita Rebase ni pregunta por él.
Las quince comprobaciones
Cada hallazgo que imprime la herramienta lleva el id de abajo, la razón por la que importa y el SQL que lo arregla. La severidad es el nivel más grave que puede reportar una comprobación — varias se rebajan según lo que encuentren.
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.
Cómo arreglarlo →Policy grants unconditional access
policy-always-trueA permissive policy whose USING or WITH CHECK expression is always true.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →RLS enabled with no policies (denies everything)
rls-enabled-no-policiesA table with row-level security enabled but not a single policy defined.
Cómo arreglarlo →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.
Cómo arreglarlo →Table privileges granted to PUBLIC
grant-to-publicA SELECT/INSERT/UPDATE/DELETE privilege granted to PUBLIC on a table.
Cómo arreglarlo →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.
Cómo arreglarlo →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.
Cómo arreglarlo →Qué aspecto tiene una ejecución
Cada hallazgo nombra la relación, dice quién puede alcanzarla y qué obtendría, e imprime el SQL que la cierra. Sale con código distinto de cero por encima de la severidad que elijas, así que encaja en 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
¿Es seguro ejecutarlo en una base de datos de producción?
Es de solo lectura, y de forma estricta. Solo ejecuta SELECT contra los catálogos del sistema —pg_class, pg_policies, pg_proc, information_schema— y nada más. Nunca lee tus datos, no escribe nada, no modifica ninguna configuración y no realiza ninguna petición de red más allá de la cadena de conexión que le proporciones. No hay telemetría ni subida de datos.
¿Hace falta usar Rebase?
No. No sabe nada de tu framework ni necesita nada de tu código base: lee la base de datos. Funciona con Supabase, Neon, RDS, Cloud SQL, un contenedor en tu portátil o cualquier otra cosa que hable el protocolo de Postgres. No necesita Rebase ni comprueba si existe.
¿Con qué rol necesita conectarse?
Con un rol que pueda leer los catálogos —normalmente un superusuario, el propietario de la tabla o alguien con BYPASSRLS—. Esto es intencionado y el informe lo indica al principio: el escaneo lee el catálogo real precisamente porque la seguridad a nivel de fila (RLS) no puede limitarlo, por lo que los resultados describen lo que experimentan otros roles, no lo que hace esta conexión.
¿En qué se diferencia del linter de Supabase?
Hay bastante solapamiento en los casos evidentes y merece la pena ejecutar ambos. Las comprobaciones de esta herramienta que no suelen aparecer en otros sitios son las indirectas: una vista o vista materializada que ignora la RLS de su tabla base porque se ejecuta con los permisos de su propietario, una tabla de unión many-to-many que queda desprotegida entre dos extremos protegidos, o una columna sin cualificar dentro de una subconsulta EXISTS que Postgres vincula a la tabla interna —lo que convierte un filtro de tenant en una tautología aunque parezca totalmente correcto—.
¿Puedo ejecutarlo en CI?
Sí —ese es precisamente su lugar natural—. Termina con un código distinto de cero cuando hay incidencias en el umbral que elijas o por encima, genera JSON para su procesamiento automático y permite omitir comprobaciones individuales por id para que una excepción justificada no rompa cada build. Los id son una API pública estable exactamente por este motivo.
¿Por qué una empresa de backend regala un escáner de seguridad?
Porque la tesis de Rebase es que la autorización pertenece a la base de datos y no a la capa que tiene delante, y una herramienta que audita RLS en Postgres es esa tesis en una forma que puedes ejecutar contra tus propios datos en lugar de leerla. Es realmente útil, mires o no el resto de este sitio web — y si no encuentra nada, habrá sido una buena tarde y no nos debes nada.
Y arréglalo una sola vez, en código.
Un escáner te dice dónde están mal las policies. Rebase las compila desde tus definiciones de collection, así que la siguiente se revisa en un pull request antes de llegar a ninguna base de datos.