Herramienta gratuita · sin cuenta

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.

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

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.

Cómo arreglarlo →

Policy grants unconditional access

policy-always-true

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

Cómo arreglarlo →

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.

Cómo arreglarlo →

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.

Cómo arreglarlo →

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.

Cómo arreglarlo →

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.

Cómo arreglarlo →
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.

Cómo arreglarlo →

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.

Cómo arreglarlo →

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.

Cómo arreglarlo →
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.

Cómo arreglarlo →

RLS enabled with no policies (denies everything)

rls-enabled-no-policies

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

Cómo arreglarlo →

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.

Cómo arreglarlo →

Table privileges granted to PUBLIC

grant-to-public

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

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

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

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

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.

~pnpm dlx @rebasepro/cli init