CVE-2025-48757 es una vulnerabilidad de Lovable con puntuación crítica (CVSS 9.3) que dejó expuestas más de 170 aplicaciones. El problema: cualquier persona, sin iniciar sesión, podía leer —y en muchos casos modificar— datos de tu base de datos: nombres, emails, claves de API de terceros e incluso el estado de los pagos. La causa no es un bug oculto de Lovable, sino un patrón que comparten casi todas las apps "vibe-coded": confiar toda la seguridad de la base de datos a las políticas RLS de Supabase… y dejarlas incompletas. En esta guía verás qué pasó exactamente, cómo comprobar en cinco minutos si tu app tiene el mismo agujero, cómo cerrarlo con SQL concreto y qué hacer si ya has expuesto datos.
Qué pasó exactamente
Lovable genera apps donde el navegador habla directamente con la base de datos (Supabase) mediante llamadas REST, usando la clave pública anon. Eso está bien diseñado siempre que cada tabla tenga políticas de Row-Level Security (RLS) que decidan qué filas puede ver o tocar cada usuario. La vulnerabilidad apareció porque muchos proyectos se desplegaban con tablas sin RLS o con RLS insuficiente. Un atacante solo tenía que editar la petición REST —cambiar un filtro en la URL— para pedir "todas las filas" de una tabla que debería estar protegida.
Para entender por qué basta con editar una URL hay que saber cómo funciona Supabase por dentro. Delante de tu base de datos Postgres hay una capa llamada PostgREST que convierte cada tabla en un endpoint HTTP: GET /rest/v1/customers?select=* devuelve filas de la tabla customers. La clave anon no es un secreto: viaja en el JavaScript de tu app y la ve cualquiera. Lo único que decide qué filas salen por ese endpoint son las políticas RLS de la tabla. Sin ellas, la respuesta es la tabla entera.
Fue reportada en marzo de 2025 por Matt Palmer y Kody Low, y de forma independiente por Danial Asaria, con divulgación pública el 29 de mayo de 2025 (detalle técnico del reporte, ficha CVE). Cualquier proyecto de Lovable con base de datos creado antes del 15 de abril de 2025 podía estar afectado.
Cronología resumida
- Marzo de 2025: primeros reportes privados a Lovable; se identifican apps publicadas con tablas legibles sin sesión.
- 15 de abril de 2025: fecha de corte que Lovable da para los proyectos afectados; los generados después incluyen cambios en cómo se crean las tablas.
- 29 de mayo de 2025: divulgación pública y asignación del CVE con puntuación crítica.
- Después: Lovable añade avisos de seguridad en el editor, pero la responsabilidad de las políticas sigue siendo de quien publica la app.
Por qué te afecta aunque no uses Lovable
Este no es un problema exclusivo de Lovable. Cualquier app que hable directamente con Supabase desde el navegador —da igual si la montaste con Bolt, v0, Cursor, Replit o a mano— depende de que RLS esté bien puesto en todas las tablas. La app se ve perfecta, el login funciona, pero la seguridad de verdad vive en la base de datos, no en la interfaz. Ocultar un botón en el frontend no protege nada: la petición REST sigue ahí para quien sepa mirar.
Lo mismo vale para Firebase con reglas en "modo test", para PocketBase sin reglas de colección o para cualquier backend-como-servicio al que el navegador llame directamente. El patrón es siempre el mismo: la IA que generó tu app sabe hacer que funcione, pero rara vez escribe las reglas que impiden que funcione para quien no debe.
Cómo saber si tu app es vulnerable (5 minutos)
Prueba 1: la petición en incógnito
- Abre las DevTools (F12) → pestaña Network mientras usas tu app y mira las llamadas a
*.supabase.co/rest/v1/. Copia una de esas URLs completa (lleva la claveapikeyen la cabecera; en la URL verás la tabla y el filtro). - Ábrela en una ventana de incógnito, sin sesión. Si te devuelve datos, cualquier visitante también los obtiene.
- Prueba a pedir "de más": si una llamada trae tus datos, quita el filtro de la URL (el
?user_id=eq.…). Si te devuelve filas de otros usuarios, no tienes RLS efectivo.
Prueba 2: el panel de Supabase
Entra en tu proyecto → Table Editor. Cada tabla sin RLS lleva una etiqueta roja que dice "RLS disabled". Después ve a Authentication → Policies: una tabla con RLS activado pero sin ninguna política lo deniega todo (bien para los datos, pero tu app dejará de funcionar), y una con una política using (true) está tan abierta como si no tuviera RLS. El panel también tiene un Security Advisor (en la sección Advisors) que lista exactamente estas tablas.
Prueba 3: una consulta SQL
En el SQL Editor del panel, esta consulta lista las tablas del esquema public (el que expone la API) que no tienen RLS activado:
select c.relname as tabla
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'r'
and not c.relrowsecurity;
Si devuelve alguna fila con datos reales dentro, esa tabla está abierta por la API ahora mismo.
Cómo arreglarlo, paso a paso
1. Activa RLS en todas las tablas
No solo en las "importantes". La regla es denegar por defecto: sin una política que lo permita, nadie accede. Para cada tabla del esquema public:
alter table public.customers enable row level security;
Al hacerlo, tu app dejará de ver datos hasta que escribas políticas. Es lo esperado: ahora la puerta está cerrada y vas a abrir solo lo necesario.
2. Escribe políticas explícitas por operación
El patrón más habitual es "cada usuario ve y edita solo sus filas", comparando auth.uid() con la columna que guarda al dueño. Para una tabla customers con una columna user_id:
create policy "customers: leer las propias"
on public.customers for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "customers: crear las propias"
on public.customers for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "customers: editar las propias"
on public.customers for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "customers: borrar las propias"
on public.customers for delete
to authenticated
using ((select auth.uid()) = user_id);
Tres detalles que marcan la diferencia: to authenticated deja fuera a los visitantes anónimos; with check impide que alguien inserte o edite filas a nombre de otro (sin él, un usuario podría crear datos con el user_id de otro); y envolver auth.uid() en un select hace que Postgres lo evalúe una vez por consulta en lugar de una vez por fila, lo que en tablas grandes multiplica el rendimiento. Añade un índice sobre user_id si no lo tienes.
3. Nunca uses la clave service_role en el frontend
Salta RLS por completo: con ella, todas las políticas que acabas de escribir no existen. Esa clave vive solo en el servidor (funciones Edge, un backend tuyo, un worker). Si alguna vez estuvo en el código del cliente, considérala filtrada y rótala desde el panel. Supabase ofrece además claves nuevas (sb_publishable_… para el navegador y sb_secret_… para el servidor) que hacen la separación más evidente.
4. Vuelve a probar
Repite la prueba de incógnito: la petición sin sesión tiene que devolver una lista vacía o un error 401. Después entra con dos cuentas distintas y comprueba que ninguna ve los datos de la otra. Si tienes tests automáticos, ese es el caso que merece uno.
Errores típicos al arreglarlo
- Políticas con
using (true). La IA las propone a menudo "para que funcione". Dejan la tabla igual de abierta que sin RLS. - Olvidar
with check. Protege la lectura pero permite escribir a nombre de otro. - Tablas de "catálogo" con datos personales. Una tabla de perfiles pública para que se vean los nombres acaba exponiendo emails o teléfonos. Expón solo las columnas necesarias a través de una vista.
- Vistas que se saltan RLS. En Postgres una vista se ejecuta con los permisos de quien la creó. Si la creas desde el panel, puede devolver filas que las políticas de la tabla base prohibirían. Créalas con
security_invoker = true(Postgres 15 o superior) o revísalas una a una. - Storage sin políticas. Los buckets tienen sus propias reglas. Un bucket público sirve cualquier archivo a quien tenga la URL; los documentos privados van en buckets privados con políticas por usuario.
- Arreglar solo producción. Si tienes un proyecto de desarrollo con datos copiados, está igual de expuesto.
Si ya has expuesto datos
Primero cierra la puerta (los pasos de arriba). Después rota cualquier clave que estuviera en tablas legibles: claves de API de terceros, tokens, webhooks. Revisa los logs de la API en el panel de Supabase para estimar qué se ha leído y desde cuándo. Si entre los datos hay información personal de usuarios europeos, el RGPD exige valorar si hay que notificar a la autoridad de protección de datos en 72 horas y, en casos de riesgo alto, a los propios usuarios. Documenta lo que has encontrado y lo que has hecho: te lo pedirán si alguien pregunta.
Un prompt para arreglarlo con tu IA
Si trabajas con Cursor, Claude o el propio chat de Lovable, este prompt suele dar un buen resultado: "Lista todas las tablas del esquema public de mi proyecto Supabase. Para cada una, activa Row-Level Security y escribe políticas separadas para select, insert, update y delete limitadas al rol authenticated, de modo que cada usuario solo acceda a las filas cuya columna user_id coincida con (select auth.uid()). Usa with check en insert y update. Dime qué tablas no tienen columna de dueño para decidirlas a mano. No uses using (true) en ningún caso."
Cómo lo comprueba Scanaris
Scanaris analiza tu web en modo solo-lectura y detecta la huella de este patrón: endpoints de Supabase accesibles desde el navegador, CORS permisivo, claves y tokens expuestos en el JavaScript y datos que la app sirve sin sesión. Si le das una connection string de solo lectura, además audita tabla por tabla si RLS está activado y si hay políticas. Pega tu URL y te dice, en segundos y ordenado por gravedad, qué está a la vista de cualquiera —antes de que lo encuentre otro.
Preguntas frecuentes
¿Lovable ya lo ha arreglado?
Lovable cambió cómo genera las tablas y añadió avisos, pero una app publicada antes del 15 de abril de 2025 sigue como la dejaste: las políticas no se añaden solas. Y en cualquier proyecto, nuevo o viejo, cada tabla que crees después necesita las suyas.
¿Activar RLS rompe mi app?
Al activarlo sin políticas, sí: todo queda denegado. Es el comportamiento correcto. Escribe las políticas del paso 2 y la app vuelve a funcionar, pero solo para quien debe.
¿Puedo proteger la tabla desde el frontend?
No. El frontend es código que corre en el ordenador del visitante y puede modificarlo a voluntad. Lo único que no puede saltarse son las reglas que aplica la base de datos.
¿Y si mi app no tiene usuarios, solo una tabla pública de contenido?
Entonces una política de lectura pública es legítima: for select to anon, authenticated using (true). Lo que no debe existir es una política de escritura abierta ni columnas con datos personales en esa tabla.