“Un archivador en un vestíbulo público con un cartel de 'solo personal', y sin cerradura.”
El cliente no está de tu lado
A las herramientas de IA modernas les encanta un patrón en el que el navegador habla directamente con una base de datos alojada como Supabase o Firebase. Es rápido de construir y luce de maravilla en la demo. El problema es el modelo de seguridad: el navegador viene con una clave de la base de datos, y el navegador está totalmente bajo el control del usuario. Cualquiera puede abrir las herramientas de desarrollador, copiar esa clave y consultar la base de datos directamente, saltándose tu app, tus pantallas y cualquier comprobación que viviera en el código del front end.
navegador (cualquiera) tu UI dice base de datos ┌───────────────┐ 'puedes ver ┌─────────────┐ │ anon key ────┼─────────── tu fila' ────X ─────▶│ cada fila │ │ devtools open │ (solo front-end) │ para todos │ └───────────────┘ ◀────── devuelve TODAS ────────└─────────────┘
Así que una regla aplicada en tu componente de React, "muestra solo los pedidos del usuario actual", no es un control de seguridad. Es una preferencia de visualización. La base de datos entregará encantada los mismos datos a una consulta directa que nunca carga tu componente. La decisión de acceso tiene que vivir donde el usuario no pueda alcanzarla: en la propia base de datos.
Qué es realmente Row-Level Security
Row-Level Security (RLS) traslada la decisión de acceso a la base de datos, por fila. Con ella activada, la base de datos rechaza toda petición por defecto, y tú escribes políticas que dicen exactamente qué filas puede ver o cambiar un usuario dado. La política clásica: un usuario puede leer una fila solo si la columna de propietario de la fila coincide con su id autenticado. Ahora la consulta directa que se saltaba tu UI no recibe nada, porque quien decide es la base de datos, no el navegador.
-- Turn RLS on, THEN add a policy. RLS on with no policy = deny everything. alter table orders enable row level security; create policy "owners read their own orders" on orders for select using ( (select auth.uid()) = user_id );
Esa es toda la idea: denegar por defecto y luego conceder de forma estrecha. Es el mismo principio de mínimo privilegio que en todas partes: parte de "nadie puede tocar nada" y abre la puerta más pequeña que haga funcionar la funcionalidad.
Los errores que siguen filtrando con RLS 'activado'
Activar RLS es necesario pero no suficiente. Estas son las formas en que las apps se filtran de todos modos, todas las cuales una herramienta de IA producirá alegremente:
- RLS activado en algunas tablas, olvidado en otras: la tabla que añadiste la semana pasada no tiene reglas y está abierta de par en par.
- Una política tan permisiva que lo concede todo (using (true)), que es lo mismo que no tener política pero con pasos extra.
- Enviar la clave service-role / admin al cliente: esa clave se salta RLS por completo, así que todas las políticas quedan sin efecto.
- Comprobar la propiedad contra un valor enviado por el cliente, en lugar del id de auth verificado en el servidor: el cliente simplemente envía otro valor.
Cómo comprobarlo de verdad
No confíes en que está hecho porque el interruptor está activado. Verifícalo como lo haría un atacante:
- Lista todas las tablas. Confirma que RLS está activado Y tiene una política real y acotada en cada una, no solo activado.
- Consulta como usuario anónimo con la clave pública y confirma que no obtienes nada que no deberías.
- Inicia sesión como segundo usuario y confirma que no puedes leer ni escribir las filas del primero.
- Busca en tu bundle de cliente y en la config de entorno la clave service-role/admin. No debería estar ahí.
Son quince minutos de trabajo y es la comprobación de seguridad de mayor palanca que puedes ejecutar sobre una app construida con IA. El CVE de Lovable fue, en el fondo, una versión a escala industrial de saltársela.
Una línea por cada uno
- Los patrones navegador-a-base-de-datos incluyen una clave que el usuario controla; cualquier comprobación en el front end es decoración, no seguridad.
- Row-Level Security traslada la decisión a la base de datos, por fila: denegar por defecto y luego conceder de forma estrecha.
- RLS 'activado' sigue filtrando por tablas olvidadas, políticas demasiado permisivas, una clave service-role en el cliente o confiar en ids enviados por el cliente.
- Verifica como un atacante: cada tabla tiene una política real, el anónimo no recibe nada, un segundo usuario está bloqueado, ninguna clave admin en el cliente.
Adónde ir ahora