En resumen
Una vista de Postgres se ejecuta con los permisos de su propietario, salvo que se cree con security_invoker = true. En Supabase el propietario suele ser postgres, que se salta RLS, así que una vista sobre una tabla protegida devuelve filas que la propia tabla oculta. En mi stack local la clave pública obtuvo 0 filas de la tabla y 3 de 3 de una vista simple sobre ella; con security_invoker = true la vista también devolvió 0. En Postgres 15 y posteriores, actívelo en cada vista de un esquema expuesto.
Mi verificador de seguridad de Supabase gratuito tiene una regla llamada “Vistas que se saltan RLS”, y el Security Advisor de Supabase informa de lo mismo como security_definer_view. Es fácil pasarlo por alto, porque la tabla de la que se filtran los datos parece protegida.
¿Por qué una vista de Supabase se salta RLS?
Una vista es una consulta guardada. Por defecto, Postgres comprueba los permisos y RLS de las tablas que contiene como el propietario de la vista, no como quien la llama. La guía de RLS de Supabase lo dice sin rodeos: las vistas se saltan RLS por defecto porque suelen crearse con el usuario postgres, y una vista sobre una tabla protegida entrega todas las filas que sus políticas debían retener.
La Data API sirve las vistas como si fueran tablas. Si la vista está en un esquema expuesto y anon o authenticated pueden hacer select sobre ella, la clave pública la lee.
¿Qué devolvió la vista en mi prueba?
En un stack local de Supabase (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) creé una tabla orders con tres clientes ficticios, activé RLS sin ninguna política y creé una vista order_summary sobre ella con un grant de select para anon. Después llamé a ambas con la publishable key:
| Petición con la publishable key | Resultado |
|---|---|
GET /rest/v1/orders (RLS activado, sin política) |
[] |
GET /rest/v1/order_summary (vista simple) |
las 3 filas, con nombres y correos |
GET /rest/v1/order_summary tras security_invoker = true |
[] |
Misma tabla, misma clave. Solo el ajuste de la vista cambió lo que se devolvió.
¿Cómo se corrige?
En Postgres 15 y posteriores, que es lo que ejecuta hoy cualquier proyecto de Supabase, haga que la vista compruebe RLS como quien la llama:
alter view public.order_summary set (security_invoker = true);
En una vista nueva, póngalo en la definición:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Entonces quien llama también necesita select sobre la tabla subyacente, y las políticas de la tabla deciden las filas. Si una vista debe agregar datos que quien la llama no puede ver fila por fila, como un recuento público, déjela fuera del esquema expuesto y sírvala mediante una función con un resultado acotado.
Para listar las vistas de public que todavía se ejecutan como su propietario:
select c.relname as view
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
and not coalesce(
(select bool_or(option = 'security_invoker=true' or option = 'security_invoker=on')
from unnest(c.reloptions) as option), false);
En mi stack, con una vista simple y dos con security_invoker = true, devolvió solo la simple.
Las vistas materializadas no admiten RLS en absoluto, así que el verificador las señala aparte en la API, bajo “Vistas materializadas o tablas foráneas en la API”. Qué más esquiva las políticas, desde la secret key hasta las funciones SECURITY DEFINER, está en mi lista de seguridad para apps Lovable, y cómo demostrar las propias políticas está en cómo probar Supabase RLS. Una política que busca equipos mediante una función auxiliar, sin recursión, está en Políticas RLS de Supabase: ejemplos probados con la API. Una función security definer necesita un ajuste más, un search_path fijo; por qué search_path = public no bastó en mi prueba está en Supabase “function search path mutable”: qué significa.