Fundamentos de seguridad
Supabase Row Level Security está activado. Tu tabla sigue pública.
Activar Supabase Row Level Security no protege una tabla. Lo hacen tus políticas, y la que arregló tu app rota puede dejar entrar a cualquiera.
En resumen
- En Supabase, el interruptor y las reglas son dos cosas distintas. Activado sin regla no deja pasar a nadie; activado con la regla equivocada no detiene a nadie.
- La regla que hace funcionar de nuevo una app rota suele ser la que permite cualquier petición, venga de quien venga.
- Lee la política, no el interruptor. Una sola palabra decide si un desconocido puede leer tu tabla.
Activaste Row Level Security porque algo te lo dijo: el advisor de Supabase, una checklist, el resultado de un escaneo, alguien en un Discord. Ahora el interruptor está verde en tu panel. Y aun así te dicen que tu tabla sigue siendo legible por desconocidos.
Aquí está la parte que guía tras guía cuenta mal: el interruptor no protege nada. Decide que tus reglas se comprueben. Las reglas son otra cosa, las tienes que escribir tú, y la regla más rápida de escribir (la que hace que una app rota vuelva a funcionar) deja entrar a todo el mundo.
Row Level Security está activado en Supabase. ¿Cómo sigue pública mi tabla?
Porque activarlo y decidir quién entra son dos pasos distintos, y solo el primero es un interruptor.
Piensa en el interruptor como en poner a alguien en la puerta. Accionarlo no decide quién pasa. Decide que ahora alguien comprueba una lista. Tus políticas son esa lista. Una lista vacía deja fuera a todos; una lista que dice «todos» no deja fuera a nadie. Ambas cosas son Row Level Security activado, y tu panel muestra el mismo verde en los dos casos.
Por eso el ajuste por sí solo responde muy poco, y por eso nuestro escáner nunca le pregunta a Supabase si está activado. Le pregunta a la tabla. Envía la petición que enviaría un desconocido, con la clave pública que viaja dentro de tu app, y mira si vuelve una respuesta. Pide un recuento en lugar de las filas, así que descubre que la puerta se abrió sin leer nada de lo que hay detrás.
La política que arregló tu app es probablemente el problema
En el momento en que activas Row Level Security tu app deja de mostrar datos, y lo que hiciste después para que volviera a funcionar es lo que merece una mirada.
Esa secuencia es completamente normal, y es donde esto se tuerce. Con el ajuste activado y ninguna política escrita, Postgres (el motor de base de datos sobre el que corre Supabase) rechaza por defecto todas las peticiones, así que tus listas vuelven vacías y tus pantallas se quedan en blanco. Algo tiene que entrar en la lista. Si le pediste a Cursor o a Lovable que lo arreglara, o pegaste el primer fragmento que hizo desaparecer el error, lo que tienes ahora probablemente se parece a esto:
CREATE POLICY "Enable read access for all users"
ON public.profiles
FOR SELECT
USING (true);
USING (true) es la condición que una fila tiene que cumplir para que la base
de datos la entregue. Todas las filas cumplen true. Ahí dentro hay un segundo
detalle fácil de pasar por alto: sin cláusula TO, una política se aplica a
public, y public incluye por igual a los visitantes con sesión iniciada y a
los desconocidos.
Así que la app vuelve a funcionar, nada da error, y la tabla es exactamente tan legible como lo era antes de empezar.
Eso arregla la lectura, y solo la lectura. Si tu app además guarda en esta tabla, lo siguiente que te encuentras es new row violates row-level security policy, que es el mismo ajuste rechazando una escritura, y ninguna policy de lectura lo quita.
Los cuatro estados en los que puede estar una tabla
Dos de ellos son seguros y dos no, y el interruptor no te dice cuáles. «Clave
publicable» abajo es la que sí pertenece a tu app: sb_publishable_… en los
proyectos nuevos de Supabase, anon en los antiguos.
| Row Level Security | La política | Tu app | Un desconocido con tu clave publicable de Supabase |
|---|---|---|---|
| Apagado | da igual, no se consulta | Funciona | Lee todas las filas |
| Encendido | ninguna escrita | Rota | No lee nada |
| Encendido | USING (true) | Funciona | Lee todas las filas |
| Encendido | USING (auth.uid() = user_id) | Funciona | Lee solo las suyas |
Las dos columnas centrales son el par que pilla a la gente. Mismo ajuste, misma insignia verde, resultados opuestos, y la diferencia es una palabra dentro de una regla que casi nadie abre.
Si prefieres no leer cada política tú mismo, nuestro escaneo gratuito le hace a tu base de datos en vivo la misma pregunta que un desconocido y te dice qué tablas respondieron. Tarda unos 20 segundos y no necesita cuenta: escanea tu app.
«Authenticated» no es lo mismo que «tuyo»
Una política que permite authenticated permite a todo el que tenga cuenta, lo
que (si tu app tiene un formulario de registro abierto) es todo el que esté
dispuesto a rellenarlo.
Esta es la versión más sutil del mismo error, y sobrevive a muchas revisiones
porque parece cuidadosa. TO authenticated USING (true) se lee como una
restricción, y lo es: excluye a quien nunca se registró. Lo que no hace es
impedir que uno de tus clientes lea las filas de otro cliente, que suele ser lo
que querías decir con «privado».
La regla que sí lo hace nombra a quién pertenece la fila:
CREATE POLICY "Users read their own rows"
ON public.orders
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
auth.uid() es quien está preguntando. user_id es la columna de la fila que
dice de quién es. La fila vuelve cuando esas dos coinciden, y se queda donde
está cuando no.
Cómo revisar tus propias tablas en dos minutos
Abre Supabase, ve a Authentication → Policies y lee la expresión que hay dentro de cada política en lugar de la insignia que hay junto a cada tabla.
Tres cosas que buscar:
- Una política cuya condición sea
true. Decide, tabla por tabla, si estarías cómodo con esos datos en una página abierta. Para una lista de artículos publicados, sí. Para cualquier cosa con una persona dentro, no. - Una política sin cláusula
TO. Se aplica a todo el mundo, con sesión o sin ella, aunque el resto de la regla parezca muy concreto. - Una tabla con el ajuste activado, sin políticas, y una app que aun así
funciona. Esa combinación significa que algo está llegando a tus datos por
otra vía, y la explicación habitual es una clave secreta (
sb_secret_…, oservice_roleen un proyecto antiguo), que ignora todas las políticas que has escrito. Qué claves de API son seguras en tu frontend cuenta cómo distinguirla de la segura.
La otra comprobación a la que se recurre es abrir la app en una ventana privada sin iniciar sesión, y merece la pena hacerla, pero conviene saber qué demuestra. Tu app decide qué dibuja. Tu base de datos decide qué entrega. Son decisiones distintas, y un desconocido que se salta tus pantallas se lleva la segunda.
Qué aspecto tiene cuando te está pasando
Ninguno. Esa es la forma de este problema, y por eso se queda ahí durante meses.
No aparece ningún error en tu builder. Nada va más lento, ninguna pantalla se rompe, no llega ningún correo. Tu app se comporta exactamente igual que el día que la lanzaste, porque desde su lado no ha cambiado nada: siempre tuvo permiso para leer esas filas. Lo que cambió es que todos los demás también lo tienen, usando la clave que viaja en el código que tu sitio envía a cada visitante.
Cuando aflora, aflora de lado. Una clienta pregunta cómo supo alguien algo que
solo sabía tu app. Una lista de direcciones que nunca publicaste aparece en
algún sitio. Y si la regla generosa cubre también la escritura (FOR ALL en
lugar de FOR SELECT), entonces cualquiera puede además cambiar y borrar
filas, que es la versión que la gente descubre como una tabla que de pronto está
vacía.
Las reglas también se desplazan. Una migración, un cambio de esquema, otro arreglo de madrugada que necesitaba que los datos cargaran: cualquiera de ellos puede aflojar una política sin decirlo, y por eso esto merece una segunda mirada más adelante y no solo una ahora. Vigilar eso es parte de lo que hace Reeve Care, aunque un recordatorio en tu calendario cumple la misma función.
Qué hacer esta semana
Qué hacer
- Abre Authentication → Policies en Supabase y lee la condición de cada política, tabla por tabla. La insignia de la tabla no es la respuesta.
- Para cada tabla con personas dentro (usuarios, perfiles, pedidos, mensajes), comprueba que la regla nombre al dueño de la fila en lugar de permitir a todos.
- Sustituye cualquier
USING (true)en esas tablas por una regla que compareauth.uid()con la columna del dueño, y confirma después que tu app sigue cargando. - Añade la cláusula
TOque querías poner. Una política sin ella se aplica a los desconocidos igual que a los visitantes con sesión. - Si una tabla tiene el ajuste activado, ninguna política y tu app sigue mostrando sus datos, averigua qué lo está sorteando antes de tocar nada más.
Empieza por la tabla que más te avergonzaría si fuera una página abierta, y arregla esa hoy. La checklist de seguridad de 10 minutos cubre esto junto a lo demás que conviene mirar en una app recién lanzada, y la guía de seguridad de Supabase repasa qué más se suele dejar abierto.
Preguntas frecuentes
Activé Row Level Security y mi app dejó de mostrar datos. ¿He roto algo?
No, eso es el ajuste funcionando. Con Row Level Security activado y ninguna política escrita, Postgres (el motor de base de datos que hay debajo de Supabase) rechaza por defecto todas las peticiones, incluidas las de tu propia app. La solución es añadir una política que describa quién debe ver qué filas. El error que hay que evitar es añadir una que permita a todo el mundo, porque esa es la versión que hace funcionar la app y deja la tabla abierta.
¿USING (true) es alguna vez la política correcta?
Sí, para datos que son públicos de verdad. Una tabla de artículos publicados, un catálogo de productos, una lista de locales en un mapa: están pensados para que cualquiera los lea, y una política que lo permite es correcta. Deja de serlo en el momento en que la tabla tiene personas dentro. Pregúntate si estarías cómodo publicando el contenido de esa tabla en una página abierta, y deja que la respuesta decida la política.
¿Row Level Security me protege si se filtró mi clave secreta?
No. Una clave secreta de Supabase (sb_secret_ en proyectos nuevos, service_role en los antiguos) se salta Row Level Security por completo, que es para lo que existe. Todas las políticas que escribiste se ignoran, en todas las tablas. Si esa clave está en tu frontend, tus políticas no están haciendo nada por ti, y rotar la clave es lo primero, antes que cualquier trabajo sobre las reglas.
¿Necesito Row Level Security si mi app ya tiene pantalla de inicio de sesión?
Sí. Tu pantalla de acceso controla tu app, y tu app no es la única forma de llegar a tu base de datos. Supabase da a cada proyecto una dirección web que responde a peticiones directamente, y la clave para hablar con ella está en el código que tu app envía a cada visitante. Las políticas son la parte que se aplica venga la petición por la puerta que venga.