Saltar al contenido

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.

Vlad Tkachenko8 min de lectura

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 SecurityLa políticaTu appUn desconocido con tu clave publicable de Supabase
Apagadoda igual, no se consultaFuncionaLee todas las filas
Encendidoninguna escritaRotaNo lee nada
EncendidoUSING (true)FuncionaLee todas las filas
EncendidoUSING (auth.uid() = user_id)FuncionaLee solo las suyas
La marca bajo cada columna no sigue al interruptor de arriba. Dos de estos están encendidos y uno de ellos lo entrega todo.

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_…, o service_role en 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.

Las pantallas de tu app no son la valla. La misma tabla puede mostrar dos filas a través de tu app y entregar las seis a una petición que nunca la abrió.

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 compare auth.uid() con la columna del dueño, y confirma después que tu app sigue cargando.
  • Añade la cláusula TO que 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.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

¿No sabes cómo está tu propia app?

Haz un escaneo gratuito y obtén una nota clara de la A a la F en unos 20 segundos. Sin cuenta y sin tarjeta.

Analizar mi app gratis

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.