Saltar al contenido

Seguridad de apps Lovable: 12 controles y 8 que añadí

Actualizado

Ver como MarkdownAbrir en ClaudeAbrir en ChatGPTAbrir en Perplexity

La pregunta también se copia. Si el chat se abre vacío, péguela.

En resumen

Una app de Lovable es tan segura como su base de datos de Supabase, y “RLS está activado” demuestra muy poco. Pruebe con dos usuarios de dos empresas, mantenga los roles fuera de los metadatos del usuario, mantenga la clave secreta fuera del navegador y revise por separado los grants, las vistas, el storage, las funciones y los límites de peticiones, porque cada uno se salta sus políticas a su manera. Abajo está la lista que ejecuto, el prompt que puede pegar en Lovable o en Claude y ocho controles que añadí después de que la app de un cliente me mostrara lo que los primeros doce no ven.

Soy Denys, ingeniero full-stack y de IA. Este mes arreglé la app de un cliente hecha con Lovable. Todo corría sobre Supabase, como de costumbre, y había tantas correcciones de seguridad que la app parecía hecha de ellas. Esta es la lista que ahora repaso en cada proyecto de Lovable o Supabase antes de decir que los datos de los clientes están protegidos.

¿Es seguro Lovable? Los problemas de seguridad hasta ahora

La plataforma construye y aloja su app, pero que la app sea segura depende sobre todo de la base de datos de Supabase que hay detrás, porque la app lee y escribe esa base de datos directamente desde el navegador. El registro público muestra los dos tipos de problema:

Cuándo Qué pasó Fuente
Marzo de 2025 CVE-2025-48757: un escaneo de 1.645 apps de muestra de Lovable halló 303 endpoints en 170 de ellas que entregaban datos a cualquiera con la clave pública, porque RLS faltaba o era demasiado laxo Divulgación de Matt Palmer, mayo de 2025
Febrero de 2026 Una app alojada en Lovable expuso 18.697 registros de usuarios; una comprobación de acceso en sus funciones dejaba pasar a visitantes anónimos y bloqueaba a los usuarios con sesión. Lovable dijo que parte del código no lo generó Lovable The Register, 27 de febrero de 2026
3 de febrero – 20 de abril de 2026 Una regresión de la plataforma permitió que otros usuarios de Lovable leyeran el historial de chat y el código fuente de proyectos públicos Publicación de Lovable sobre el incidente, 22 de abril de 2026

Los dos primeros son huecos a nivel de app, del tipo que atrapan los controles de abajo. El tercero fue de Lovable y Lovable lo corrigió; lo suyo es mantener las claves fuera de los chats y del código (control 3) y rotar las que estuvieran ahí. Lovable ejecuta un análisis de seguridad antes de publicar, pero actuar sobre él depende de usted, y cuando se publicó el CVE ese análisis comprobaba que existiera una política, no que fuera correcta. Así que “¿es seguro Lovable?” se reduce a si su base de datos pasa los controles de abajo.

¿Por qué “RLS está activado” no basta en una app de Lovable?

Una app de Lovable habla con la base de datos directamente desde el navegador. No hay un backend propio en medio: la página llama a la Data API de Supabase con una clave pública y Postgres decide qué devuelve (qué esquemas sirve esa API es un ajuste aparte, tratado en Supabase exposed schemas). Así que toda regla que proteja datos de clientes tiene que vivir en la base de datos, y la base de datos tiene varias puertas.

El row level security es una de ellas. Los grants deciden si un rol puede tocar una tabla, las políticas deciden qué filas, y tres cosas se saltan las políticas por completo: la clave secreta, las vistas creadas sin security_invoker y las funciones SECURITY DEFINER. No es hipotético. La propia guía de seguridad de Lovable le pide revisar las políticas RLS que configura, y CVE-2025-48757, arriba, es lo que pasa cuando nadie lo hace.

Cuatro caminos de una petición a sus datos. Solo el primero lo comprueban sus políticas RLS. Petición normal: Navegador → Grant → Política RLS → Filas propias; Clave secreta en una edge function: Navegador → Edge function → Clave secreta → Todas las filas; Vista sin security_invoker: Navegador → Vista → Derechos del dueño → Todas las filas; Función SECURITY DEFINER: Navegador → RPC → Derechos del dueño → Todas las filas.
Figura 1. Solo el primer camino lo comprueban sus políticas. Los otros tres se ejecutan con derechos que se saltan RLS. Fuente: documentación de Supabase sobre RLS y seguridad de la API, octubre de 2026.

El prompt: péguelo en Lovable o en Claude

Puede pegarlo tal cual. Después compruebe el resultado usted mismo: a la IA le encanta decir “hecho” cuando no lo está, y un prompt que hace desaparecer un error puede lograrlo ampliando el acceso.

Audita este proyecto en seguridad, corrige lo que encuentres y dime qué cambiaste:

1. Prueba con dos usuarios de dos empresas. El usuario A intenta leer, editar y borrar registros de B por id, por la API, la búsqueda y las exportaciones. Todo debe fallar.
2. RLS en cada tabla, las políticas comprueban el dueño de la fila o la empresa. "Con sesión iniciada" no basta.
3. La service role key nunca en el frontend. Las edge functions que la usan comprueban al usuario y filtran por empresa ellas mismas, la clave se salta RLS.
4. Roles, plan e id de empresa nunca desde los metadatos del usuario, los usuarios pueden editarlos. Guárdalos en una tabla donde solo escribe el servidor.
5. Cuando una petición envía dos ids (proyecto + tarea), comprueba que la tarea pertenece a ese proyecto.
6. Revoca todos los privilegios de tabla a anon y authenticated, devuelve solo los que necesitan las políticas. RLS no frena TRUNCATE. Cada migración que crea una tabla incluye su GRANT: desde el 30 de octubre de 2026 Supabase ya no concede grants a las tablas nuevas automáticamente, y la app recibe el error 42501.
7. Todas las vistas con security_invoker = true, si no se saltan RLS.
8. Buckets de storage: los usuarios leen y escriben solo sus propios archivos, sin buckets públicos para datos privados.
9. Funciones SECURITY DEFINER: NULL debe significar denegar, no permitir.
10. Límites de peticiones en login, registro, restablecimiento y OTP, guardados en la base de datos, no en memoria.
11. Valida toda la entrada en el servidor.
12. Ejecuta el security advisor de Supabase y corrige cada aviso.

Los 12 controles y cómo demostrar cada uno

1. Dos usuarios en dos empresas

Cree al usuario A en la empresa 1 y al usuario B en la empresa 2, cada uno con algunos registros. Inicie sesión como A e intente leer, editar y borrar los registros de B por id, por la API, por la búsqueda y por cada exportación. Cada intento debe devolver nada o un error. Es la única prueba que atrapa la mayor parte de lo que sigue, porque prueba lo que hace el atacante, no lo que usted escribió.

Dos usuarios, dos empresas. El usuario A de la empresa 1 va a por los registros de la empresa 2. Cada camino debe devolver nada. Por id con la API: Usuario A → Id del registro de B → Leer, editar, borrar → 0 filas o un error; Por búsqueda: Usuario A → Buscar texto de B → Resultados → Sin filas de B; Por una exportación: Usuario A → Exportación → Archivo → Sin filas de B.
Figura 2. La prueba de las dos empresas. Se aprueba si ningún camino devuelve nada de la empresa 2.

Añada un tercer usuario que pertenezca a las dos empresas. Una política del tipo “quien llama es miembro de la empresa de la fila” deja a ese usuario leer las filas de la empresa 2 mientras trabaja en la empresa 1. En otra app de Supabase, mi sonda con un usuario así devolvió el documento de la otra empresa hasta que la consulta también filtró por la empresa de la petición. Lo escribí en un usuario en dos orgs que RLS no aisló.

2. RLS en cada tabla, limitado al dueño o a la empresa

auth.uid() is not null significa “cualquier usuario con sesión”, que en una página de registro pública significa cualquiera. Cada política compara el dueño o la empresa de la fila con quien llama. Liste las tablas que aún tienen RLS desactivado:

select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;

3. La clave secreta nunca en el frontend

La service role key, y la más nueva clave sb_secret_, se saltan todas las políticas. Busque ambas en el JavaScript compilado y rote cualquier clave que alguna vez se haya publicado. Las edge functions que la usan deben comprobar quién es el usuario y filtrar por empresa ellas mismas, porque con esa clave nada más lo hará.

4. Roles, plan e id de empresa nunca desde los metadatos del usuario

raw_user_meta_data es lo que el cliente envió a signUp, y los usuarios pueden cambiarlo después. En otra app, el trigger de nuevo usuario leía de ahí el rol de la invitación, así que cualquiera que se registrara podía pasar el rol de propietario y quedar como Owner. Guarde roles, planes e ids de empresa en una tabla donde solo escribe el servidor, o en app_metadata. La historia completa: un rol tomado de los metadatos del registro.

5. Dos ids en una petición: compruebe que van juntos

Cuando una petición lleva un id de proyecto y un id de tarea, autorizar el proyecto no basta. Un usuario con derechos sobre el proyecto A envía A más el id de una sección del proyecto B, y la fila acaba en B. RLS no lo detecta, porque el insert marca el id de proyecto del propio atacante, que es justo lo que comprueba la política. Compruebe que el hijo pertenece al padre antes de escribir.

6. Grants: revocar y devolver solo lo necesario

Los grants y las políticas son dos capas separadas. Añadir políticas no quita un grant, por eso la guía de RLS de Supabase indica revocar y volver a conceder de forma explícita. En otro proyecto, la imagen supabase/postgres 17.6.1 había dado a anon y authenticated TRUNCATE, REFERENCES, TRIGGER y MAINTAIN sobre todas las tablas de public, y ninguna política se aplica a TRUNCATE. La Data API no tiene un endpoint de TRUNCATE, así que no es un borrado con una sola petición, pero ningún rol de cliente lo necesita, y cualquier función que ejecute SQL con ese rol lo hereda.

revoke all on public.orders from anon, authenticated;
grant select, insert, update on public.orders to authenticated;

select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by table_name;

Una fecha cambia este control. Supabase deja de conceder grants a las tablas nuevas automáticamente: en los proyectos nuevos desde el 30 de mayo de 2026, y en todos los proyectos existentes desde el 30 de octubre de 2026, incluidos los que creó Lovable. Después, una tabla que Lovable o una migración cree sin un GRANT explícito es invisible para la app: la petición falla con 42501 y la función que usa la tabla deja de funcionar. Por eso cada migración que crea una tabla lleva ahora sus grants, junto a enable row level security y las políticas.

Dos cosas siguen igual. Las tablas existentes conservan los grants que tienen, así que revocar lo que no necesita sigue siendo cosa suya. Y el cambio solo retira select, insert, update y delete: en el proyecto de Lovable, con los nuevos valores por defecto, las tablas nuevas seguían llegando con TRUNCATE, REFERENCES, TRIGGER y MAINTAIN para anon y authenticated. Revocar todo primero y conceder después sigue siendo el orden seguro.

7. Toda vista con security_invoker = true

Una vista se ejecuta con los derechos de su dueño, y en Supabase el dueño suele ser postgres, así que una vista sobre una tabla protegida entrega todas las filas que las políticas retienen. En Postgres 15 y posteriores:

alter view public.customer_summary set (security_invoker = true);

8. Storage: solo archivos propios, sin bucket público para datos privados

Un bucket público sirve cualquier archivo a quien tenga la URL. Los archivos privados van en un bucket privado con políticas sobre storage.objects que comprueben el dueño o la empresa, normalmente a partir de la primera carpeta de la ruta. Tras mover un proyecto, cuente las políticas de storage vivas: en una app, todas las subidas fallaron después de una mudanza porque la migración de políticas nunca se ejecutó y la tabla tenía RLS activado con cero políticas.

9. Funciones SECURITY DEFINER: NULL significa denegar

En PL/pgSQL, if not (has_permission or assignee_id = auth.uid()) no lanza excepción cuando assignee_id es null: toda la condición es null y if la salta. La misma expresión es segura dentro de una política, que trata null como denegar. En una app eso dejó que un rol con todos los permisos desactivados actuara sobre las 3683 tareas, porque ninguna tenía responsable. Envuelva la condición en coalesce(…, false). Detalles: un NULL en un IF NOT de PL/pgSQL saltó el control.

10. Límites de peticiones guardados en la base de datos

Limite el login, el registro, el restablecimiento de contraseña y el OTP, y guarde el contador en Postgres. En la app de Lovable los límites vivían en una variable dentro de la edge function. Probé el proyecto alojado el 17 de octubre de 2026: cada petición corría en un isolate nuevo, así que el contador en memoria siempre valía 1 y el límite nunca podía activarse. supabase functions serve en local reutiliza el isolate, por eso el mismo código pasó todas las pruebas locales.

Dónde vive el contador decide si salta. Límite de peticiones en una edge function alojada de Supabase. Contador en la memoria de la función: Petición → Isolate nuevo → Contador = 1 → Nunca limitado; Contador en Postgres: Petición → claim_rate_limit → Fila compartida → 429 al llegar al límite.
Figura 3. Mi sonda de una edge function alojada de Supabase, 17 de octubre de 2026: cada petición inició un isolate nuevo.

La clave importa tanto como el almacén. En la plataforma alojada, el último salto de X-Forwarded-For es el propio proxy de Supabase, así que usarlo como clave mete a todos los llamantes en un solo cubo, y x-client-ip lo fija el cliente. En mi sonda, cf-connecting-ip llevaba el cliente real. La corrección fue una función SECURITY DEFINER claim_rate_limit(bucket, key, max, window) que solo puede ejecutar el service role, serializada con un advisory lock, que guarda un hash de la IP o del id de usuario en lugar del valor en bruto.

11. Valide toda la entrada en el servidor

Compruebe lo que compruebe el formulario, la API acepta todo lo que envía el cliente. Longitudes, formatos, enums y propiedad se comprueban en la base de datos (constraints, triggers) o en la edge function, nunca solo en React.

12. El Security Advisor de Supabase, cada aviso

Abra Advisors, luego Security Advisor, y corrija cada hallazgo. Señala tablas con RLS desactivado, vistas que se saltan RLS, políticas que leen metadatos del usuario, políticas permisivas using (true) y funciones con un search_path mutable. No ve los controles 1, 5, 9 ni 10, porque tratan de lógica, no de configuración.

¿Qué me enseñó la app de Lovable que los 12 no ven?

Esto salió de la app del cliente y de sus revisiones. Nada de esto aparece en un escáner.

  1. Campos que el cliente no debe fijar. El cliente envió status: 'approved' por su cuenta, y se saltó la moderación. Una política UPDATE cuyo with check es más laxo que su using también deja a un usuario mover una fila a otro dueño. Una política no puede calcular un valor, así que un trigger BEFORE INSERT OR UPDATE recalcula status, rol, precio y empresa a partir de los datos del propio servidor e ignora lo que envió el cliente.
  2. Cada camino de escritura, no solo el insert principal. Tras arreglar el insert principal, el mismo bypass seguía en adjuntos, comentarios, restaurar un elemento borrado con soft delete y contadores. Liste todos los escritores de una columna protegida antes de arreglar el primero.
  3. Listas de permitidos, no de prohibidos. Los registros falsificados costaron cinco rondas de revisión, porque cada corrección bloqueaba una combinación y dejaba abierta una hermana. Un trigger que permite solo las combinaciones listadas de tipo y estado cerró toda la clase de una vez.
  4. Las funciones también son una API. Toda función en public se puede llamar por /rpc desde cualquier rol con EXECUTE, y RLS no se aplica a las funciones. Dé a cada función SECURITY DEFINER set search_path = '', revoque EXECUTE a public, anon y authenticated, y concédalo solo al rol que lo necesita.
  5. Revocar EXECUTE puede romper páginas públicas. Postgres comprueba EXECUTE en toda función usada por cada política que se aplica al rol, incluso en una rama que nunca se ejecutaría. Cuando anon perdió EXECUTE sobre is_admin(), toda lectura anónima falló con 42501. Las políticas solo para admin van to authenticated, y las lecturas públicas tienen su propia política sin la rama de admin.
  6. Registro con confirmación de correo activada. Con la confirmación activada, signUp no devuelve sesión, así que cualquier escritura de perfil justo después corre como anon y no escribe nada en silencio. Los proyectos de Lovable suelen tener la autoconfirmación activada, lo que oculta el bug. Pase los campos en signUp({ options: { data } }), deje que el trigger de nuevo usuario los valide y los escriba, y trate esos metadatos como no confiables (control 4).
  7. Pruebas que pasan por la razón equivocada. El stack local y el proyecto alojado tenían privilegios por defecto distintos, así que “anon no puede insertar” pasaba en local porque faltaba el grant, no porque RLS funcionara. Una prueba negativa solo significa algo cuando existe el grant positivo. Compare pg_default_acl en local y en producción.
  8. La base de datos viva, no los archivos de migración. Tras una migración o una mudanza de proyecto, compare lo que realmente está vivo: pg_policies, pg_get_functiondef de cada guarda y los grants de arriba. En otra app, una migración nueva copió el cuerpo de una función antigua y deshizo en silencio una corrección posterior.

¿Una app de Lovable cumple el RGPD?

Lovable no hace que una app cumpla o incumpla el RGPD; lo decide lo que la app hace con los datos personales, y una revisión de seguridad no lo responde. Estas son las seis brechas que reviso en una app de Lovable Cloud, cada una con su solución:

  1. Liste cada encargado del tratamiento. Lovable Cloud ejecuta la base de datos, la autenticación, el almacenamiento y las funciones sobre Supabase, Lovable aloja el sitio y puede enviar los correos de inicio de sesión, y el Lovable AI Gateway pasa texto a un proveedor de modelos. Añada cada servicio de correo, IA o scraping que llamen sus funciones. Cada uno que reciba datos personales necesita un contrato de encargo del tratamiento (DPA), y uno fuera de la UE necesita además un mecanismo de transferencia. La región de alojamiento no aparece en el código, así que pregúntela.
  2. Borre un usuario que tenga contenido. Las claves foráneas de lo que escribió pueden bloquear el borrado, y entonces una solicitud de supresión no se puede completar. Haga que esas claves usen cascade o set null, añada un trigger de borrado que elimine también sus registros de correo y tokens de inicio de sesión, y mantenga una prueba de base de datos que borre a un usuario así.
  3. Fije un plazo de conservación. Los registros de correo, el seguimiento de visitas, las colas de correos fallidos y las filas con borrado lógico suelen guardar datos personales sin fecha de fin. Una tarea programada que borre esas filas definitivamente tras un plazo fijo, junto con sus archivos, cierra la mayor parte.
  4. Seguimiento de correos solo con consentimiento. Registrar aperturas y clics por destinatario requiere consentimiento según las normas de ePrivacy: un indicador de consentimiento, desactivado por defecto, con la marca de tiempo fijada por el servidor, no por el cliente.
  5. Saque las direcciones de los logs de las funciones. Los logs de las edge functions no se limpian, así que un console.log con la dirección de un destinatario la deja ahí. Registre ids y recuentos.
  6. Active la confirmación de correo. Con la autoconfirmación, cualquiera puede registrarse con una dirección que no es suya y meter en la app la dirección de una persona real. El punto 6 de la lista anterior cubre la parte del registro.

Para revisar su propia app: busque claves de API en las funciones para listar los servicios, borre un usuario de prueba con contenido, busque @ en los logs de las funciones y pida a cada proveedor su contrato de encargo. Esto no es asesoramiento legal.

¿Cómo evitar que vuelva?

Cada control de arriba es una consulta o una prueba, así que guárdelos en el repositorio y ejecútelos antes de cada despliegue. Las pruebas pgTAP de Supabase cubren grants, políticas y funciones; la prueba de las dos empresas va junto a ellas. Si su app tiene roles y una pantalla de permisos, tampoco basta una prueba por política: yo demuestro cada casilla de permiso contra una copia de producción, como describo en cómo probar Supabase RLS, donde 30 de 87 casillas resultaron no hacer nada. La Auditoría de seguridad de Supabase gratuita ejecuta 46 reglas de OWASP Top 10:2025 sobre los roles, grants, políticas y ajustes de auth de un proyecto, así que úsela para la primera pasada; la prueba de las dos empresas y todo lo de su interfaz siguen siendo manuales.

Lo que esta lista no cubre: pagos y webhooks, secretos en su historial de Git, cabeceras de seguridad en el host, copias de seguridad. Importan, pero son otra lista.

Si heredó una app de Lovable o Supabase y no está seguro de cuáles de estos controles pasa, esta auditoría es el comienzo de un rescate de proyecto. El caso del seguimiento de horas para una firma de auditoría muestra cómo son las comprobaciones de roles en una app en producción.

¿Encontró un hueco que esta lista no cubre?

Me gustaría saberlo. Comparto con gusto cómo asegurar apps de Lovable y Supabase.

Escríbame

© 2026 Denys Zaichenko. Artículo original: https://madsalt.tech/es/blog/lovable-app-security-checklist/