← Blog
·9 min read

Guía de Seguridad RLS en Supabase: Errores Comunes y Cómo Arreglarlos

Row Level Security en Supabase es tu primera línea de defensa. Esta guía cubre los errores más comunes — incluyendo los que comete la IA — y cómo verificar que tus políticas funcionen.

Rod

Founder & Developer

Row Level Security (RLS) en Supabase es una de esas cosas que parece opcional hasta que te das cuenta de que sin ella, cualquier usuario autenticado puede leer los datos de todos los demás usuarios. Esta guía cubre los errores más comunes que vemos en repos que usan Supabase — incluyendo los patrones que Cursor, Bolt y otras herramientas de vibe coding generan por defecto.

Escaneamos decenas de repos con Supabase. El patrón más frecuente no es que RLS esté desactivado — es que está habilitado pero configurado de forma que no protege lo que el developer cree que protege. Ese es el hueco más peligroso: la falsa sensación de seguridad.


Qué es RLS y por qué importa

RLS (Row Level Security) es una función de PostgreSQL que agrega una capa de control de acceso a nivel de fila. En lugar de que las restricciones de acceso estén solo en tu código de aplicación, RLS las pone directamente en la base de datos.

Sin RLS, si un usuario hace una query directa a tu endpoint de Supabase con el anon key:

// Sin RLS, esto devuelve TODOS los registros de la tabla
const { data } = await supabase.from("orders").select("*");

Con RLS y una política correcta:

// Con RLS + auth.uid(), esto solo devuelve las órdenes del usuario actual
const { data } = await supabase.from("orders").select("*");
// Supabase aplica automáticamente: WHERE user_id = auth.uid()

La diferencia es que incluso si tu código de aplicación tiene un bug que no filtra por usuario, la base de datos sí lo hace. Es defensa en profundidad — si una capa falla, la otra sostiene.


Error #1: RLS habilitado sin políticas

Este es el error más común y el más confuso. Habilitas RLS pero no defines ninguna política.

-- Habilitaste RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
 
-- Pero no definiste ninguna política
-- Resultado: NINGÚN usuario no privilegiado puede leer la tabla

El comportamiento por defecto de PostgreSQL cuando RLS está habilitado sin políticas es denegar todo acceso. Tu app deja de funcionar. Los devs que se topan con esto buscan rápido cómo hacer que "funcione de nuevo" y encuentran el service role key — lo que nos lleva al error #2.

La solución correcta:

-- Política para que usuarios vean solo sus propias órdenes
CREATE POLICY "users_see_own_orders"
  ON orders
  FOR SELECT
  USING (auth.uid() = user_id);
 
-- No olvides INSERT, UPDATE, DELETE si los necesitas
CREATE POLICY "users_insert_own_orders"
  ON orders
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);

Error #2: Service role key en el cliente

Este es el patrón que más daño hace. Alguien no puede hacer que RLS funcione, encuentra que el service role key lo resuelve, y lo mete en el frontend "temporalmente".

// MAL: service role key en el cliente — bypasea RLS completamente
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // NUNCA en el cliente
);
 
// BIEN: usar el anon key en el cliente, RLS protege los datos
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // key pública, RLS aplica
);

El service role key tiene permisos de superusuario. Bypasea todas las políticas RLS. Si ese key llega al bundle del cliente — y en Next.js cualquier variable que no empieza con NEXT_PUBLIC_ debería quedarse en el servidor — cualquier usuario puede abrir las DevTools, copiar el key, y hacer queries directas a tu base de datos con permisos de admin.

Si tienes SUPABASE_SERVICE_ROLE_KEY en algún lugar de tu código de cliente, detén lo que estás haciendo y cámbialo ahora.

En Next.js, el service role key solo debe usarse en Server Components, API routes, y server actions — nunca en componentes que se ejecuten en el cliente.


Error #3: Políticas que cubren SELECT pero no INSERT/UPDATE/DELETE

Las herramientas de IA generan políticas RLS para SELECT casi siempre. Las políticas para INSERT, UPDATE y DELETE las olvidan con frecuencia.

-- Lo que Cursor suele generar (incompleto)
CREATE POLICY "users_see_own_data"
  ON profiles
  FOR SELECT
  USING (auth.uid() = id);
 
-- Lo que falta — cualquier usuario puede insertar datos con cualquier user_id
-- y modificar registros que no son suyos si sabe el ID

Una política SELECT te protege de que los usuarios lean datos ajenos. Pero sin políticas para INSERT y UPDATE, un usuario puede crear registros haciéndose pasar por otro usuario o modificar registros existentes que no le pertenecen.

Las cuatro políticas que necesitas en casi toda tabla:

-- SELECT: ver solo los propios
CREATE POLICY "select_own"
  ON profiles FOR SELECT
  USING (auth.uid() = id);
 
-- INSERT: solo crear con tu propio user_id
CREATE POLICY "insert_own"
  ON profiles FOR INSERT
  WITH CHECK (auth.uid() = id);
 
-- UPDATE: solo modificar los propios
CREATE POLICY "update_own"
  ON profiles FOR UPDATE
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);
 
-- DELETE: solo borrar los propios
CREATE POLICY "delete_own"
  ON profiles FOR DELETE
  USING (auth.uid() = id);

Error #4: Políticas demasiado permisivas

El opuesto del error #1 — políticas que están definidas pero que permiten demasiado.

-- MAL: política que permite a cualquier usuario autenticado ver todo
CREATE POLICY "authenticated_can_read_all"
  ON orders
  FOR SELECT
  USING (auth.role() = 'authenticated');
 
-- BIEN: usuarios solo ven sus propias órdenes
CREATE POLICY "users_see_own_orders"
  ON orders
  FOR SELECT
  USING (auth.uid() = user_id);

La primera política solo verifica que el usuario esté autenticado — no que sea dueño de los datos. Cualquier usuario con sesión activa puede leer todas las órdenes de todos los usuarios.

Este error aparece cuando la IA genera código "que funciona" para el caso de uso del developer (ver mis órdenes) sin modelar el riesgo de un usuario malicioso (ver las órdenes de todos).


Error #5: No testear las políticas RLS

Definir políticas no garantiza que funcionen correctamente. El error más frecuente que no se detecta hasta que alguien lo explota: las políticas usan el nombre de columna incorrecto.

-- La tabla tiene user_id pero la política referencia author_id
-- Supabase crea la política sin error, pero no protege nada
CREATE POLICY "users_see_own"
  ON posts FOR SELECT
  USING (auth.uid() = author_id); -- author_id no existe, retorna NULL → política no aplica

Cómo testear tus políticas RLS:

-- En el SQL Editor de Supabase, puedes simular un usuario específico
SET LOCAL role TO authenticated;
SET LOCAL request.jwt.claims TO '{"sub": "uuid-del-usuario"}';
 
-- Esta query debe retornar solo los registros del usuario simulado
SELECT * FROM orders;

También puedes hacer un test desde tu cliente con el anon key intentando acceder a datos de otro usuario — si los devuelve, la política tiene un problema.


Cómo verificar tu configuración de Supabase

El Supabase Dashboard tiene una sección de "Auth Policies" donde puedes ver todas las tablas y si tienen RLS habilitado y cuántas políticas tiene cada una. Una tabla con RLS habilitado y 0 políticas es una señal de alerta.

Data Hogo analiza las políticas RLS como parte del scan completo. Detecta:

  • Tablas sin RLS habilitado
  • Tablas con RLS habilitado pero sin políticas (deniegan todo)
  • Patrones de código que usan el service role key en el cliente
  • Uso del anon key con queries que no pasan por las políticas correctamente

Si tienes un repo con Supabase y no has hecho un escaneo de seguridad, hay una probabilidad real de que tengas uno de estos cinco errores. Los encontramos en la mayoría de repos que escaneamos.

Escanea tu configuración de Supabase gratis →


El checklist de RLS que funciona

Antes de hacer deploy de cualquier feature que toca la base de datos:

  • RLS habilitado en todas las tablas con datos de usuarios
  • Políticas definidas para SELECT, INSERT, UPDATE y DELETE (según lo que uses)
  • Las políticas referencian auth.uid() con el nombre de columna correcto
  • El service role key solo está en variables de servidor (sin NEXT_PUBLIC_)
  • Testeado desde el cliente con el anon key intentando acceder a datos de otro usuario
  • Las migraciones están en control de versiones

Para contexto sobre por qué el control de acceso roto es tan crítico en aplicaciones web, la guía de OWASP sobre control de acceso roto cubre los patrones de ataque con más detalle.


Preguntas frecuentes

¿Qué es Row Level Security (RLS) en Supabase?

RLS (Row Level Security) es una función de PostgreSQL que controla qué filas puede ver o modificar cada usuario. En Supabase, se usa para que los usuarios solo puedan acceder a sus propios datos. Sin RLS, cualquier usuario con el anon key puede leer toda la tabla.

¿Cómo sé si mis políticas RLS de Supabase están mal configuradas?

La forma más directa es testear desde el cliente con el anon key — si puedes leer datos de otros usuarios sin estar autenticado como ellos, hay un problema. También puedes escanear tu repo con Data Hogo, que analiza las políticas RLS y detecta tablas sin políticas o con políticas permisivas.

¿Qué pasa si habilito RLS pero no defino ninguna política?

Si habilitas RLS en una tabla sin definir ninguna política, el comportamiento por defecto de PostgreSQL es denegar todo acceso a usuarios no privilegiados. Esto puede hacer que tu app deje de funcionar para usuarios normales. El error común es usar el service role key para esquivar esto, lo que crea un hueco de seguridad diferente.

¿El service role key de Supabase bypasea RLS?

Sí. El service role key tiene permisos de superusuario y bypasea todas las políticas RLS. Nunca debe usarse en el cliente (frontend) — solo en código del servidor. Si el service role key llega al navegador, cualquier usuario puede hacer queries directas con permisos de admin a tu base de datos.

¿Cursor y otras herramientas de IA generan código de Supabase seguro?

Las herramientas de IA generan código de Supabase que funciona, pero frecuentemente con huecos de RLS. El patrón más común: generan la política para SELECT pero olvidan las políticas para INSERT, UPDATE y DELETE. O usan el service role key para hacer funcionar algo rápidamente sin resolver el problema de permisos de fondo.

supabaserlsrow level securityseguridadbase de datosvibe-codingcursorpostgresql