PlisPlasNotas de taller
enunplisplas.com Hablemos
PlisPlasBlog
Hablemos

Checklist de seguridad para tu app de Lovable, Bolt o Cursor: lo que hay que probar antes de abrir la puerta

Equipo PlisPlas8 min de lectura

Tu app funciona. Eso no dice nada de quién más puede usarla. Antes de pasar el enlace a clientes de verdad, hay una lista corta de comprobaciones que separan un lanzamiento de una filtración. Ninguna necesita ser experto en seguridad: necesitan media tarde, dos cuentas de prueba y una terminal.

Las herramientas de vibe coding generan apps que se conectan directamente desde el navegador a Supabase o Firebase. Es un buen diseño, pero tiene una consecuencia: el navegador del usuario es territorio enemigo. Todo lo que tu app puede hacer desde ahí, lo puede hacer también cualquiera con la consola del navegador abierta. La seguridad no está en la interfaz; está en las reglas de la base de datos y en el servidor.

Si buscas por qué tu app falla en producción, eso lo contamos en las 9 causas. Esto es otra cosa: la lista para marcar con un bolígrafo antes de lanzar.

Checklist · 8 pasos por gravedad
Crítico · día unoImportanteÚltima capa
  1. 01
    Crítico · día unoBase de datos: cada fila con su dueñoSin sesión, con la clave pública, pide una tabla: debe volver []. Después, con la sesión de A, intenta leer, editar y borrar filas de B.
  2. 02
    Crítico · día unoClaves: la pública puede verse, la secreta jamásBusca service_role y sb_secret en el bundle de producción y en el repositorio.
  3. 03
    Crítico · día unoFunciones de servidor: no te fíes de lo que llegaLlama a cada función sin cabecera de autorización, con el token de otro usuario y con identificadores ajenos. Las tres llamadas deben fallar.
  4. 04
    Crítico · día unoAlmacenamiento: «público» significa públicoAbre la URL de un documento privado en una ventana de incógnito: debe dar error.
  5. 05
    ImportanteLogin: verificación y redireccionesConfirmación de email activada y URLs de redirección exactas en producción.
  6. 06
    ImportanteLímites: que un bot no te arruine la nocheUn bucle de cien llamadas seguidas a tu función más cara. Debería empezar a rechazarlas mucho antes de la centésima.
  7. 07
    Crítico · día unoEl repositorio: secretos y dependenciasgit log --all -- .env. Si sale algo, rota la credencial.
  8. 08
    Última capaCabeceras: la última capa, no la primeracurl -I https://tudominio.com y mira qué cabeceras vuelven.
Una tabla abierta es una filtración el día uno; una cabecera de menos, no.
Paso 01 / 08

Base de datos: cada fila con su dueño

En Supabase, la seguridad a nivel de fila (RLS) decide qué filas puede leer o escribir cada usuario. Con RLS activado y sin políticas, la tabla no devuelve nada por la API pública: cerrada por defecto. Sin RLS, la clave pública lo abre todo. En Firebase el equivalente son las reglas de Firestore y de Storage.

  • RLS activado en todas las tablas del esquema público, sin excepción. Supabase incluye un Security Advisor en el panel que avisa de las que no lo tienen.
  • Políticas de propiedad, no de sesión. auth.uid() is not null solo comprueba que hay alguien conectado; lo que quieres es (select auth.uid()) = user_id. En Firebase, la misma trampa: request.auth != null deja a cualquier usuario registrado leer los datos de todos.
  • Updates con `WITH CHECK`. USING filtra qué filas puedes tocar; WITH CHECK controla cómo pueden quedar. Si la política no restringe una columna, el usuario puede cambiarla en su propia fila: un campo role o plan editable desde el navegador es una escalada de privilegios. Esas columnas, fuera del alcance del cliente.
  • Vistas revisadas. En Postgres las vistas se saltan RLS por defecto. Créalas con security_invoker = true o no las expongas.
  • Nada de reglas de «modo prueba». En Firebase, allow read, write: if true o una regla con fecha de caducidad es para el primer día, no para producción.

Cómo probarlo: sin sesión, con la clave pública que ves en el código del navegador, pide una tabla: curl 'https://TU-PROYECTO.supabase.co/rest/v1/pedidos?select=*' -H 'apikey: CLAVE_PUBLICA'. Debe volver []. Después crea dos cuentas, A y B, y desde la consola del navegador con la sesión de A intenta leer, editar y borrar filas de B. Todo debe fallar. Un escáner que te dice «RLS activado» no te dice que las políticas sean correctas; esta prueba sí.

Cómo probarlo · RLS
Prueba 01 · Sin sesión
clave pública
curl 'https://TU-PROYECTO.supabase.co/rest/v1/pedidos?select=*' -H 'apikey: CLAVE_PUBLICA'
Debe volver[]
Prueba 02 · Dos cuentas
Sesión de AFilas de B
  • Leer✕ Debe fallar
  • Editar✕ Debe fallar
  • Borrar✕ Debe fallar
Un escáner que te dice «RLS activado» no te dice que las políticas sean correctas; esta prueba sí.
Paso 02 / 08

Claves: la pública puede verse, la secreta jamás

Supabase tiene dos familias. La pública (anon en el sistema antiguo, sb_publishable_… en el nuevo) está pensada para ir en el navegador y respeta RLS. La secreta (service_role o sb_secret_…) se salta todas las políticas. Supabase lo dice sin rodeos: nunca en un navegador, en una app publicada ni en el control de versiones.

  • Busca la clave secreta en el bundle. Abre la web en producción, pestaña Sources o Network de las herramientas de desarrollador, y busca service_role y sb_secret. También en el repositorio: git grep -n service_role.
  • Revisa las variables con prefijo público. Todo lo que empieza por NEXT_PUBLIC_ o VITE_ acaba en el navegador. Si ahí hay algo que no sea la URL y la clave pública, está expuesto.
  • Claves de terceros igual. OpenAI, Stripe secreta, Resend, Google Maps sin restringir: fuera del cliente.
  • Si una clave secreta ha estado expuesta, rótala. Moverla de sitio no basta: quien la copió la sigue teniendo.
Paso 03 / 08

Funciones de servidor: no te fíes de lo que llega

Las Edge Functions de Supabase, las Cloud Functions de Firebase o las rutas de API de Next.js suelen usar la clave secreta. Eso significa que lo que compruebe la función es la única barrera.

  • El usuario sale del token, nunca del cuerpo. Si la función recibe { userId: "…" } y actúa en su nombre, cualquiera puede mandar otro userId.
  • Permisos dentro de la función. Que el usuario esté identificado no quiere decir que pueda borrar ese pedido o ver esa factura.
  • `verify_jwt = false` solo donde haga falta, como un webhook de Stripe, y en ese caso verificando la firma del webhook.
  • Errores sin detalles internos. Un 500 con el SQL o la traza completa le enseña tu esquema a quien pruebe.

Cómo probarlo: llama a cada función con curl sin cabecera de autorización, con el token de otro usuario y con identificadores ajenos en el cuerpo. Las tres llamadas deben fallar.

Paso 04 / 08

Almacenamiento: «público» significa público

En Supabase Storage, un bucket público deja descargar cualquier fichero a quien tenga la URL. Subir, borrar o mover sigue controlado por políticas en storage.objects, pero leer no.

  • Público solo para lo que de verdad lo es: logos, imágenes de producto, avatares si te parece bien.
  • Facturas, DNI, nóminas, contratos: bucket privado y enlaces firmados con caducidad corta, generados después de comprobar permisos.
  • Rutas con el id del dueño (user_id/fichero.pdf) y políticas que lo exijan, para que A no pueda subir ni sobrescribir en la carpeta de B.
  • Límite de tamaño y tipo de fichero en la configuración del bucket.

Cómo probarlo: copia la URL de un documento privado, ábrela en una ventana de incógnito y debe dar error. Con la sesión de A, intenta subir un fichero a la ruta de B.

Paso 05 / 08

Login: verificación y redirecciones

  • Confirmación de email activada. Supabase la incluye en su checklist de producción. Sin ella, cualquiera se registra con el correo de otro; y si alguna política o regla se fía del email, peor (Firebase recomienda exigir email_verified en ese caso).
  • URLs de redirección exactas en producción. Supabase recomienda la ruta exacta para el Site URL y reservar los comodines para entornos de prueba. Borra las URLs de la vista previa de Lovable o Bolt que ya no uses.
  • SMTP propio, para que los correos lleguen desde tu dominio y no parezcan phishing.
  • Registro abierto solo si debe estarlo. Una herramienta interna no necesita que se apunte cualquiera de internet.
  • Caducidad de enlaces y códigos razonable. Supabase recomienda una hora o menos para los OTP.
Paso 06 / 08

Límites: que un bot no te arruine la noche

  • Límite por usuario y día en cada función que llame a un modelo de IA, envíe correos o SMS, o cueste dinero por llamada.
  • CAPTCHA en registro, inicio de sesión y recuperación de contraseña. Supabase lo soporta en esos tres puntos.
  • En Firebase, App Check: comprueba que la petición viene de tu app y no de un script. Complementa las reglas; no las sustituye.
  • Alertas de gasto en cada proveedor que facture por uso.

Cómo probarlo: un bucle de cien llamadas seguidas a tu función más cara. Debería empezar a rechazarlas mucho antes de la centésima.

Paso 07 / 08

El repositorio: secretos y dependencias

  • `.env` en `.gitignore` y fuera del historial. Compruébalo con git log --all -- .env. Si sale algo, no basta con borrarlo: GitHub recomienda rotar la credencial de inmediato, porque limpiar el historial es lento y no deshace la exposición.
  • Escaneo de secretos activado. GitHub lo hace gratis en repositorios públicos; para los privados, herramientas como gitleaks sirven igual en local.
  • `npm audit` y revisión de lo que la IA añadió: paquetes que no se usan, nombres raros, versiones fijadas hace años.
  • Lockfile en el repo, para que producción instale exactamente lo que probaste.
Paso 08 / 08

Cabeceras: la última capa, no la primera

Las cabeceras HTTP no arreglan una base de datos abierta, pero cierran ataques en el navegador. Se configuran en Vercel, Netlify o tu servidor en un par de líneas.

  • `Strict-Transport-Security` para forzar HTTPS.
  • `Content-Security-Policy` con los orígenes que de verdad usas, incluido frame-ancestors para que nadie meta tu app en un iframe.
  • `X-Content-Type-Options: nosniff` y `Referrer-Policy` razonable.
  • CORS de tus funciones limitado a tu dominio, no *.

Cómo probarlo: curl -I https://tudominio.com y mira qué cabeceras vuelven. Si no aparece ninguna de las de arriba, no las tienes.

El orden

Si solo tienes una tarde

  1. La prueba de las dos cuentas sobre todas las tablas y buckets.
  2. Buscar la clave secreta y cualquier clave de terceros en el navegador y en el historial de git.
  3. Llamar a cada función sin sesión y con la sesión de otro.

Esas tres cubren lo que más daño hace. El resto es importante, pero una tabla abierta es una filtración el día uno; una cabecera de menos, no.

Que la app funcione demuestra que hace lo que quieres. La seguridad es demostrar que no hace lo que no quieres.
Servicio · Relevo

¿Prefieres que lo revisemos nosotros?

Auditamos las reglas, las claves y las funciones de tu app hecha con IA, te decimos qué está abierto y lo cerramos antes de que lo encuentre otro.

Cuéntanos tu proyecto

Sigue leyendo

Todos los artículos →
Apps hechas con IA · 9 min

Mi app hecha con IA funciona en la demo y falla en producción: 9 causas

Apps hechas con IA · 8 min

¿Arreglar tu app hecha con IA o tirarla y empezar de cero? Cómo decidirlo sin tirar el dinero