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 tablaEl 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_KEYen 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 IDUna 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 aplicaCó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.
Posts Relacionados
Variables de Entorno en Vercel: Qué es Seguro y Qué No
Vercel variables de entorno seguridad — qué expone NEXT_PUBLIC_, cómo configurar env por entorno, y cómo proteger secretos en edge functions. Con ejemplos reales.
Headers de Seguridad en Next.js: Guía Completa (2026)
Configura los headers de seguridad en Next.js — CSP, HSTS, X-Frame-Options y más. Ejemplos reales en next.config.ts y por qué cada header importa.
Firebase Security Rules: Los Errores Más Comunes (y Cómo Evitarlos)
firebase rules errores que dejan tus datos expuestos: read: true global, sin validación de datos, reglas que nunca aplican. Con ejemplos y fixes reales.