Inicio › Guías › Supabase y RLS

Supabase y RLS: el error de seguridad más común en apps hechas con IA

Qué es Row Level Security, por qué en la demo no se nota que falta y cómo revisarla y arreglarla en cuatro pasos, con el SQL de cada uno.

Respuesta corta: en una app de Supabase el navegador habla directamente con la base de datos usando una clave pública, así que lo único que impide que alguien lea los datos de otra persona es Row Level Security (RLS). Para revisarlo: mira en el Security Advisor qué tablas no la tienen, actívala en todas, escribe políticas que comparen cada fila con quien la pide (auth.uid() = user_id) y pruébalo con dos cuentas. La app funciona igual con RLS que sin ella, y por eso el fallo no se ve en la demo.

El error de seguridad más común en las apps hechas con Lovable, Bolt o v0 no es un ataque sofisticado: es una casilla sin marcar. Las herramientas de IA suelen activar RLS, pero no siempre y no en todas las tablas, y a veces la activan con una política que deja pasar a cualquiera. Esta guía sale de cómo lo revisamos al llevar Calma a producción, donde seguimos comprobándolo en cada cambio.

Qué es RLS

Row Level Security es una función de Postgres, la base de datos de Supabase, que decide qué filas de una tabla puede leer, crear, cambiar o borrar cada persona. Sin RLS, los permisos son de tabla entera: quien puede leer la tabla, la lee toda.

En Supabase eso importa más que en otras arquitecturas. La app no pasa por un servidor propio que filtre los datos: el navegador consulta la base con la clave anon (o publishable), que va dentro del código que descarga cualquiera. Esa clave está pensada para ser pública, siempre que RLS esté bien puesta. Si no lo está, basta abrir la consola del navegador para leer la tabla completa.

Por qué en la demo no se nota

Porque la app funciona exactamente igual con RLS que sin ella. Tu pantalla filtra por usuario, así que cada persona sólo ve lo suyo. El problema es que la base de datos entrega lo de todos a quien lo pida sin pasar por tu pantalla. Con una sola cuenta de prueba no hay nada ajeno que ver, y el fallo no aparece hasta que hay clientes reales.

No es teórico: el fallo CVE-2025-48757 describía exactamente esto en apps generadas con Lovable (Lovable lo discute y dice que proteger los datos es responsabilidad de cada cliente).

Cómo revisarlo y arreglarlo en cuatro pasos

  1. Paso 1

    Mira qué tablas no la tienen

    En el panel de Supabase, Advisors → Security Advisor avisa de cada tabla pública sin RLS («RLS Disabled in Public»). También lo puedes preguntar en el editor SQL:

    select relname as tabla
    from pg_class
    where relnamespace = 'public'::regnamespace
      and relkind = 'r'
      and not relrowsecurity;

    Lo que buscas: cero filas. Cada tabla que salga se puede leer entera con la clave pública.

  2. Paso 2

    Actívala en todas

    Tabla a tabla:

    alter table public.pedidos enable row level security;

    Si son muchas, esta consulta te escribe una línea por cada tabla que falte, para revisarlas y ejecutarlas:

    select format('alter table public.%I enable row level security;', relname)
    from pg_class
    where relnamespace = 'public'::regnamespace
      and relkind = 'r'
      and not relrowsecurity;

    Ojo: con RLS activa y sin políticas, nadie puede leer nada. La app «se rompe» hasta el paso 3, y está bien: es lo seguro por defecto. Hazlo primero en un entorno de pruebas, no en producción con clientes dentro.

  3. Paso 3

    Escribe las políticas

    La regla más habitual es que cada persona vea y cambie sólo sus filas. Necesitas una columna con la dueña de cada fila (normalmente user_id, que apunta a auth.users) y una política por operación:

    create policy "Ver sus pedidos" on public.pedidos
      for select to authenticated
      using ((select auth.uid()) = user_id);
    
    create policy "Crear sus pedidos" on public.pedidos
      for insert to authenticated
      with check ((select auth.uid()) = user_id);
    
    create policy "Cambiar sus pedidos" on public.pedidos
      for update to authenticated
      using ((select auth.uid()) = user_id)
      with check ((select auth.uid()) = user_id);
    
    create policy "Borrar sus pedidos" on public.pedidos
      for delete to authenticated
      using ((select auth.uid()) = user_id);

    Tres detalles que marcan la diferencia:

    • to authenticated deja fuera a quien no ha iniciado sesión. Sin eso la política vale para todos los roles, también anon.
    • with check impide crear o mover una fila a nombre de otra persona.
    • (select auth.uid()), entre paréntesis, hace que Postgres lo calcule una vez por consulta y no una por fila. Añade también un índice en user_id: con miles de filas se nota.

    Lo que hay que evitar: using (true), o una política que sólo pide estar autenticado. RLS está «activa», el Security Advisor no se queja, y cualquier usuaria con cuenta lee los datos de todas las demás.

    Si una tabla no la escribe nunca el navegador (contadores, registros, lo que calcula el servidor), no le pongas políticas de escritura: que la escriban sólo tus funciones del servidor.

  4. Paso 4

    Pruébalo con dos cuentas

    Crea dos cuentas de prueba, A y B, con datos en cada una. Entra con A e intenta leer, cambiar y borrar lo de B. Con una sola cuenta el fallo no aparece nunca.

    Lo mismo en el editor SQL, haciéndote pasar por A sin cambiar nada (el rollback deshace todo):

    begin;
    set local role authenticated;
    select set_config('request.jwt.claims',
      '{"sub": "UUID-DE-LA-CUENTA-A", "role": "authenticated"}', true);
    
    select count(*) from public.pedidos;  -- sólo los de A
    update public.pedidos set estado = 'x'
      where user_id = 'UUID-DE-LA-CUENTA-B';  -- 0 filas
    rollback;

    Y sin sesión, con la clave pública, como lo haría cualquiera desde fuera:

    curl 'https://TU-PROYECTO.supabase.co/rest/v1/pedidos?select=*' \
      -H "apikey: TU-CLAVE-ANON"

    Lo que buscas: [], una lista vacía. Si salen pedidos, cualquiera los está viendo.

Lo que RLS no cubre

Con todas las tablas en verde todavía quedan cuatro puertas que conviene mirar:

  • La clave service_role se salta RLS. Sólo puede estar en el servidor: nunca en el frontend ni en una variable VITE_ o NEXT_PUBLIC_.
  • Las vistas se ejecutan, por defecto, con los permisos de quien las creó, y pueden enseñar filas que RLS ocultaría. Créalas con with (security_invoker = true); el Security Advisor también las marca.
  • Las funciones security definer también se saltan RLS. Si el navegador puede llamarlas, tienen que comprobar dentro quién llama (auth.uid()) y qué le dejan hacer.
  • Los ficheros de Storage tienen sus propias políticas, en storage.objects. Un bucket público se puede descargar entero.

Cómo lo hacemos en Calma

En Calma todas las tablas tienen RLS, y no lo comprobamos una sola vez: un test de la base de datos falla si alguna tabla de public no la tiene, y se ejecuta en el CI en cada cambio, antes de que nada llegue a producción. Las tablas que sólo escribe el servidor no tienen políticas de escritura para el navegador. En la auditoría de RLS de septiembre lo que más trabajo dio no fueron las tablas, sino las funciones: una que el navegador podía llamar dejaba escribir en el propio registro de decisiones de IA, y la pasamos al servidor.

Preguntas frecuentes

¿Qué es RLS en Supabase?

Row Level Security es la regla de Postgres que decide qué filas puede leer o cambiar cada usuario. En Supabase el navegador consulta la base directamente con una clave pública, así que RLS es la única barrera entre un usuario y los datos de los demás.

¿Lovable activa RLS automáticamente?

A menudo sí, pero no siempre, no en todas las tablas y no siempre con políticas correctas. Hay que comprobarlo tabla a tabla y probarlo con dos cuentas antes de abrir la app a clientes.

¿Es un problema que la clave anon de Supabase esté en el frontend?

No, si RLS está bien: la clave anon o publishable está pensada para ir en el navegador. El problema es una tabla sin RLS o con políticas abiertas, o cualquier otra clave en el frontend, sobre todo la service_role.

Si activo RLS, ¿se rompe mi app?

Sí, hasta que escribas las políticas: con RLS activa y sin políticas nadie puede leer nada. Por eso conviene hacerlo en un entorno de pruebas, tabla a tabla, y probar cada pantalla antes de pasarlo a producción.

¿Cómo sé si alguien ya ha leído mis datos?

Es difícil saberlo después: una lectura con la clave pública parece una consulta normal. Lo urgente es cerrar el hueco hoy. Si las tablas guardaban datos personales, revisa los registros de la API de Supabase y valora si tienes que notificar una brecha de seguridad a la AEPD (72 horas desde que lo sabes, según el RGPD).

¿Quieres revisar el resto de tu app? Sigue con el checklist de 15 cosas que revisar antes de abrirla a clientes reales.

¿Quieres que lo revisemos contigo?

En 30 minutos repasamos tu app y te decimos qué le falta para estar en producción y por dónde empezaríamos. Sin compromiso.