Evita que te hackeen si haces vibecoding
Veintiséis agujeros por los que entran en las aplicaciones hechas con IA, de lo que arreglas esta tarde a lo que hay que entender
Con este prompt avanzado cubres todas las vulnerabilidades:
Índice
1. Pon Cloudflare delante, y ya has ganado media batalla
Empiezo por esto porque es lo que más devuelve por lo poco que cuesta: media hora, gratis, y no toca una sola línea de tu código. Y gratis de verdad: Cloudflare tiene una capa gratuita que no caduca ni pide tarjeta, y ahí dentro va todo lo que necesita un proyecto pequeño. Apuntas tu dominio, activas la nube naranja y a partir de ese momento nadie habla con tu servidor directamente.
Lo que te llevas de regalo es una lista larga: la IP de tu máquina deja de ser pública, el certificado se renueva solo, los bots conocidos se caen antes de llegar, tienes reglas de límite de peticiones y un cortafuegos de aplicación con las reglas de OWASP ya escritas. Nada de esto te salva de un fallo en tu código, y lo digo en serio. Pero te quita de encima el ruido de fondo, que es el 90 % de lo que recibe una web pequeña.
2. HTTPS en todo, y que no se pueda entrar de otra forma
En 2026 esto debería sobrar y no sobra. Sigo viendo webs que responden por HTTP, o que redirigen a HTTPS pero aceptan la primera petición en claro. En esa primera petición viaja la cookie de sesión, y quien esté en la misma wifi que tu usuario se la queda.
Son tres cosas: certificado (te lo da Cloudflare o tu hosting, gratis), redirección permanente de HTTP a HTTPS, y HSTS, que es una cabecera que le dice al navegador «a esta web no vuelvas a venir en claro nunca más». Con las tres, la primera visita es la única con un hueco, y con HSTS precargado ni eso.
3. El .env no sube a Git jamás
El .env es donde viven la contraseña de tu base de datos, la clave de Stripe y el token del correo. Está pensado para no salir de tu ordenador y, aun así, es el fichero que más veces acaba en un repositorio público. No por descuido tuyo: es que nadie le dijo a Git que lo ignorase, y Git no pregunta.
El arreglo son dos líneas. Una en .gitignore que diga .env, y un .env.example al lado con los mismos nombres y los valores vacíos, para que quien clone el proyecto sepa qué le hace falta sin saber cuánto vale. En producción las claves no van en ningún fichero: van en el panel de variables de entorno de Vercel, de Railway o de donde despliegues.
4. Ninguna clave escrita dentro del código
Escribir una clave «a fuego» es ponerla tal cual en medio del código, entre comillas. Es lo primero que hace la IA cuando le pides que conecte con un servicio, y tiene su lógica: es lo que funciona al primer intento, y no tiene forma de saber que ese fichero va a acabar en GitHub el jueves.
La regla cabe en una línea: en el código va el nombre de la clave, nunca la clave. process.env.STRIPE_SECRET_KEY es un nombre; sk_live_51H seguido de cuarenta caracteres es un secreto. Y escríbelo en el fichero de reglas de tu proyecto, ese que la IA lee siempre. Una línea ahí te ahorra revisarlo en cada cambio, que es donde acabas fallando.
5. Git se acuerda de lo que borraste: rota la clave
Digamos que subiste una clave y lo ves media hora después. Borras la línea, haces commit y respiras. No has arreglado nada. Git guarda todas las versiones, y esa clave sigue ahí, a un comando de distancia para cualquiera que se baje el repositorio.
Y no es que alguien tenga que estar buscándote a ti: hay robots rastreando GitHub a tiempo completo. La media entre publicar una clave de AWS y recibir el primer intento de uso se cuenta en minutos, no en días. Lo único que arregla de verdad una clave publicada es rotarla: entras en el servicio, la invalidas y generas otra. Limpiar el historial con git filter-repo o con BFG está bien para no dejar rastro, pero es el segundo paso. Y para que no se repita, gitleaks te revisa cada commit antes de que salga de tu máquina.
6. Los registros del despliegue también cantan
Este se le escapa a todo el mundo, a mí también se me ha escapado. Tienes el .env fuera de Git, las claves en el panel del hosting, todo bien. Y entonces tu script de compilación hace un console.log de la configuración para depurar algo, y esa configuración lleva dentro la clave. Ahí queda, en el registro del despliegue, que en muchos proveedores es público si el proyecto lo es.
Pasa lo mismo con los mensajes de error que mandas a Sentry, con los avisos de la integración continua y con las capturas que pegas en un canal de Slack. Repásalo una vez: busca en tus registros del último despliegue las cuatro primeras letras de alguna de tus claves. Si aparece, ya sabes qué toca.
7. Lo que llega al navegador es público
Hay una línea muy clara en toda aplicación web: lo que se ejecuta en tu servidor y lo que se descarga el navegador. Todo lo que cruza esa línea es público. No «difícil de encontrar»: público. Está en un fichero que se abre con el botón derecho y ver código fuente, y que esté minificado no cambia nada, porque minificar no es cifrar.
Por eso los frameworks marcan qué variables pueden viajar: en Next se llaman NEXT_PUBLIC_ y en Vite VITE_. Si una clave lleva ese prefijo, dala por publicada. Muchos servicios te dan dos claves precisamente por esto, una publicable y otra secreta. La secreta no toca el navegador jamás: la llamada la hace tu servidor, y el navegador habla con tu servidor.
8. Apaga el modo depuración en producción
Un mensaje de error detallado es un regalo para quien está mirando. Cuenta qué base de datos usas, cómo se llaman tus tablas, en qué carpeta vive el código y qué versión tienes de cada librería. Con eso ya no hace falta ni buscar: los fallos conocidos de esa versión están publicados y ordenados por si alguien los necesita.
En producción el usuario ve una frase genérica y un identificador, y el detalle se queda en tus registros. Es literalmente una variable de entorno en la mayoría de frameworks, y aun así se despliega en modo depuración más veces de las que parece, porque en local funcionaba y nadie volvió a mirarlo.
9. Las páginas de prueba que se quedaron abiertas
Toda aplicación hecha deprisa tiene su cementerio: la ruta /test que devolvía datos de ejemplo, el /admin/tools que se hizo para migrar una tabla, el panel de la base de datos que se dejó accesible «solo un rato», el endpoint que reinicia el seed. Ninguno pide contraseña, porque cuando se escribieron era localhost.
Estas cosas no se encuentran adivinando: se encuentran con una herramienta que prueba dos mil rutas típicas en treinta segundos, y esas herramientas las tiene cualquiera. Haz la lista de todo lo que expone tu aplicación, mírala entera y borra lo que no debería estar ahí. Borrar, no proteger: lo que no existe no se puede olvidar de proteger.
10. Cuatro cabeceras y una política de contenido
Las cabeceras de seguridad son de esas cosas que suenan a mucho y se ponen en diez minutos. Van en la configuración de tu framework o directamente en Cloudflare, y cada una cierra un ataque concreto: que metan tu web en un iframe para que la gente pinche donde no cree, que el navegador adivine el tipo de un fichero subido, que tus URLs internas viajen al hacer clic fuera.
La que más cuesta es la política de contenido, la CSP, porque hay que decir de dónde puede cargar scripts tu página y a la primera siempre rompes algo. Compensa igual: es lo único que convierte un fallo de inyección en el navegador en una línea de error en vez de en un robo de sesión. Empieza en modo aviso, mira qué se queja durante una semana y luego la aprietas.
11. CORS: quita el asterisco
CORS es la regla que decide qué webs pueden llamar a tu API desde el navegador de un usuario. Cuando algo no funcionaba, en algún momento alguien puso un asterisco para que dejara de dar error, y ahí sigue. El asterisco quiere decir «cualquier web del mundo».
Por sí solo no es el fin del mundo, porque con el asterisco el navegador no manda cookies. El problema es la combinación que hace todo el mundo: origen abierto y credenciales activadas. Ahí cualquier página que visite tu usuario puede hacer peticiones a tu API con su sesión puesta. Pon la lista de tus dominios, que son dos, y olvídate.
12. El login no lo escribas tú
Un login parecen dos campos y una comparación, y por eso es lo primero que la gente escribe a mano. No lo es. Detrás hay contraseñas cifradas con el algoritmo correcto y no con el que sonaba bien, sesiones que caducan y se pueden revocar, un «he olvidado mi contraseña» que no sirva para entrar en cuentas ajenas, verificación del correo, segundo factor y un límite de intentos.
Todo eso está hecho, probado y auditado en Clerk, en Auth0, en Supabase Auth o en Better Auth. Ponerlo son un par de horas; hacerlo tú son semanas y aun así te va a faltar algo, porque la lista de arriba se me ha quedado corta. Y si puedes, ofrece passkeys: quitan la contraseña de la ecuación, y una contraseña que no existe no se roba, ni se reutiliza, ni se adivina a fuerza de intentos.
13. Un límite de intentos, o te prueban contraseñas gratis
Sin límite de peticiones, tu formulario de login es una máquina de probar contraseñas a costa tuya. No hace falta ser nadie ni tener nada especial: un script en el portátil de cualquiera hace cientos de intentos por segundo, y con la lista de contraseñas filtradas de siempre acierta en más cuentas de las que te gustaría.
El límite se pone en dos sitios. En Cloudflare, con una regla que diga cinco peticiones por minuto a /login desde la misma IP, y son cinco clics. Y en tu código, contando intentos por cuenta y no solo por IP, porque repartir el ataque entre mil IPs es barato y repartirlo entre mil cuentas no sirve de nada. Aplícalo también a recuperar contraseña, a los códigos de verificación y a cualquier cosa que mande un correo.
14. Lo que compruebas en el navegador, compruébalo otra vez en el servidor
La interfaz no es una medida de seguridad, es una comodidad para el usuario que va de buena fe. Quien quiere entrar no usa tu interfaz: abre las herramientas de desarrollo, mira qué peticiones haces y las repite a mano cambiando lo que le interesa. Ocultar un botón no oculta la ruta que hay detrás, y esa ruta sigue contestando igual de amable.
Así que cada comprobación que hagas en el navegador tienes que volver a hacerla en el servidor, sin excepción y aunque te parezca que sobra. El navegador decide qué se ve; el servidor decide qué se puede hacer. Si solo pudieras tener una de las dos, quédate con la del servidor: una web fea que no te vacían es mejor negocio que una bonita que sí.
15. Todo cerrado por defecto, y abres lo que toca
Casi todo el mundo protege las rutas una a una, según se acuerda. Funciona hasta la ruta número treinta, o hasta que le pides a la IA un endpoint nuevo a las once de la noche y nadie repasa la lista. El agujero casi nunca es la ruta de administración obvia: es la de exportar en CSV que se hizo para un informe y se quedó ahí.
Dale la vuelta. Que todo pida sesión por defecto y que las páginas públicas sean una lista corta y explícita: un middleware en el borde, tres o cuatro rutas abiertas y ya está. Así una ruta nueva nace cerrada, que es exactamente lo que quieres cuando no la has escrito tú. Abrir una de más se nota el mismo día, porque alguien no puede entrar; dejar una abierta sin querer no se nota nunca.
16. Cada usuario ve lo suyo y nada más
Estás en /facturas/1042 y ves tu factura. Cambias el número a 1041 y ves la de otro. Eso tiene nombre, IDOR, y es con diferencia el fallo que más me encontré cuando me dedicaba a esto. No requiere ninguna técnica ni ninguna herramienta: requiere una flecha del teclado y curiosidad, y por eso lo encuentra cualquiera sin proponérselo.
Pasa porque la consulta pregunta «dame la factura 1042» en lugar de «dame la factura 1042 de este usuario». El identificador siempre viene de fuera y nunca es de fiar; quién eres siempre sale de la sesión. Y cuando no cuadre, responde 404 en vez de 403: un «no tienes permiso» ya está confirmando que ese dato existe, y eso también es información que estás regalando.
17. La base de datos tiene su propia puerta
Supabase y Firebase te dan una clave que va en el navegador para hablar con la base de datos directamente. Es comodísimo y es parte de por qué se monta una aplicación en un fin de semana. También significa que la seguridad ya no está en tu servidor, porque no hay servidor en medio: está en las reglas de la propia base de datos, y esas vienen apagadas.
En Supabase se llama Row Level Security y hay que encenderla tabla por tabla. Sin ella, cualquiera que abra tu web tiene la clave y puede pedir la tabla entera: no hace falta ni saltarse nada, es que la base de datos te la da. En Firebase son las reglas de seguridad, y el modo de pruebas deja todo abierto durante treinta días y luego te avisa por correo, que es un correo que nadie lee.
18. Los ficheros nacen públicos si nadie dice lo contrario
Aquí es donde suelen estar las cosas de verdad delicadas: documentos escaneados, fotos de perfil, contratos, facturas. Y es la parte que menos se mira, porque subir un fichero funciona a la primera y no vuelves a pensar en ello.
Los buckets de S3, el almacenamiento de Supabase y el de Firebase tienen todos el mismo patrón: si no configuras nada, el fichero es accesible por su URL para quien la tenga, y esas URLs son adivinables más veces de las que deberían. Ponlos en privado y sirve cada fichero con un enlace firmado que caduque en unos minutos. Cuesta media hora y es la diferencia entre «se filtraron datos» y «no pasó nada».
19. Un esquema en la puerta, y tu código ya solo ve datos buenos
Todo lo que llega de fuera es una sugerencia: el cuerpo de la petición, los parámetros de la URL, las cabeceras y lo que sube el usuario. La IA escribe el camino feliz porque es el que le has descrito, y el camino feliz da por hecho que el precio es un número positivo, que el correo tiene una arroba y que la cantidad no es menos tres. Ese menos tres, en una tienda, es un abono.
La solución no es llenar el código de comprobaciones sueltas, que es lo que sale si lo vas parcheando. Es poner un esquema en la puerta: con Zod, con Pydantic o con lo que use tu lenguaje declaras qué forma tiene una petición válida y todo lo demás se cae con un 400 antes de tocar nada. Un sitio, una vez, y a partir de ahí tu código trabaja con datos en los que puede confiar.
20. Inyección de SQL
Una inyección es lo que pasa cuando el texto que escribe un usuario acaba mezclado con la orden que le mandas a la base de datos. Si construyes la consulta pegando cadenas, quien escriba en tu buscador puede escribir algo que la base de datos entienda como una orden más, y la ejecuta sin rechistar, porque no sabe distinguir lo que escribiste tú de lo que escribió él.
Se arregla siempre igual: consultas con parámetros. Mandas la consulta con huecos y los valores por separado, y la base de datos ya sabe que eso son datos y no órdenes. Cualquier ORM decente lo hace solo, así que hoy el peligro no está en el ORM: está en la única consulta que alguien escribió a mano un martes porque el ORM no llegaba. Búscala, que la tienes.
21. Inyección de NoSQL
Mucha gente cree que por usar MongoDB esto no va con ellos. Va, y de una forma más silenciosa. En MongoDB los filtros son objetos, y si metes en un filtro directamente lo que llega del usuario, puede mandarte un objeto en lugar de un texto.
El caso clásico es el login: en vez de una contraseña te mandan un operador que significa «distinto de nada», que es cierto para cualquier contraseña, y entran sin saberla. No hay comillas raras ni nada que parezca un ataque; es un JSON perfectamente válido. Se arregla igual que todo lo demás en esta lista: valida el tipo antes de usarlo. Si esperas un texto, exige un texto.
22. Lo que sube el usuario
Un formulario de subida es una puerta con la que le estás dejando escribir en tu servidor a un desconocido. La extensión del nombre no dice nada, el tipo que declara el navegador tampoco, y «solo acepto imágenes» comprobado en el navegador ya sabes que no se comprueba en ningún sitio.
El orden que funciona: límite de tamaño primero, tipo real leído del contenido y no del nombre, nombre nuevo generado por ti, guardado en un bucket privado y no en la carpeta desde la que sirves la web, y servido desde otro dominio. Ese último punto parece exagerado y no lo es: si sirves un fichero ajeno desde tu dominio, el navegador se lo cree tuyo, con tus cookies y todo.
23. Las librerías que llevan dos años sin tocarse
Tu aplicación son cuatro mil paquetes de los que has elegido ocho. Cuando aparece un fallo grave en uno de ellos se publica, se le pone número y queda documentado para siempre, con instrucciones. A partir de ese momento, cualquiera puede comprobar en diez segundos si tu web usa esa versión.
No hace falta obsesionarse. Con npm audit una vez al mes y Dependabot activado en el repositorio ya vas por delante de la mayoría. Lo que sí evitaría es lo de siempre: instalar un paquete de cincuenta descargas semanales para no escribir diez líneas. Antes de añadir una dependencia, mira cuándo se actualizó por última vez y quién hay detrás.
24. Copias de seguridad que hayas restaurado alguna vez
Esto no evita que entren, evita que te arruinen. La mayoría de los incidentes que acaban mal no acaban mal por lo que se llevaron, sino por lo que se borró. Y ahí la pregunta no es si tienes copias: es si has restaurado una alguna vez.
Tres cosas y ya está: que la copia sea automática y diaria, que esté en otro sitio y con otras credenciales (si entran en tu cuenta y las copias están en la misma cuenta, no tienes copias), y que la restaures una vez, entera, en un proyecto de pruebas. Hasta que no la restauras no sabes si funciona, y descubrirlo el día malo es una experiencia que no le deseo a nadie.
25. Tu cuenta de GitHub es la llave maestra
Puedes hacer todo lo anterior perfecto y perderlo por aquí. Tu cuenta de GitHub tiene el código; la de Vercel, las variables de entorno; la de Supabase, la base de datos; la del dominio permite mandar tu tráfico a otra parte. Es una cadena, y se rompe por donde no lleva segundo factor.
Media hora bien invertida: segundo factor en todas esas cuentas y en el correo con el que las recuperas, que es el eslabón que todo el mundo olvida. Un gestor de contraseñas para no repetir ninguna. Y revisa qué aplicaciones de terceras partes tienen permiso sobre tu GitHub, porque ahí suele haber cosas de hace tres años que ni recuerdas haber autorizado.
26. Y si ya ha pasado
Si estás leyendo esto porque ya te ha pasado, el orden importa y la tentación es hacerlo al revés. Lo primero no es entender qué ha ocurrido: es cerrar. Rota todas las claves, cierra todas las sesiones abiertas, y si sabes por dónde entraron, tapa eso antes de nada.
Después toca mirar los registros, y aquí es donde te vas a alegrar de haberlos guardado. Y luego lo incómodo: si se han llevado datos de personas, hay que decirlo. En Europa el reglamento da setenta y dos horas para notificarlo, y esconderlo sale infinitamente más caro que contarlo. Lo último, cuando ya no arde nada, es escribir qué pasó y qué has cambiado. Es la única parte de la que se aprende algo.
El repaso completo
- Cloudflare delante del dominio, con la nube naranja puesta.
- HTTPS obligatorio, HSTS y cookies con Secure y HttpOnly.
- El .env en .gitignore y las claves en el panel del hosting.
- En el código va el nombre de la clave, nunca la clave.
- Una clave que llegó a Git se rota. Borrarla no sirve.
- Ninguna clave impresa en los registros del despliegue.
- Nada secreto con prefijo público: lo que va al navegador es público.
- Modo depuración apagado y errores genéricos de cara al usuario.
- Borradas las rutas de prueba, los paneles y los endpoints de seed.
- Cabeceras de seguridad puestas y una CSP, aunque empiece en modo aviso.
- CORS con la lista de tus dominios, nunca con asterisco y credenciales.
- El login, contratado. Y passkeys si puedes.
- Límite de peticiones por IP y por cuenta, también en recuperar contraseña.
- Toda comprobación del navegador, repetida en el servidor.
- Todo cerrado por defecto y una lista corta de rutas públicas.
- El identificador viene de la URL; el permiso, de la sesión. Y 404, no 403.
- Row Level Security encendida tabla por tabla.
- Buckets privados, nombres aleatorios y enlaces que caducan.
- Un esquema de validación en la puerta de cada endpoint.
- Consultas con parámetros, nunca cadenas pegadas.
- En NoSQL, el tipo validado antes de meterlo en un filtro.
- Subidas con límite, tipo real, nombre nuevo y otro dominio.
- npm audit al mes y Dependabot encendido.
- Copias diarias, en otra cuenta, y restauradas al menos una vez.
- Segundo factor en GitHub, en el hosting y sobre todo en el correo.
- Y un plan de dos líneas para el día que pase: cerrar, mirar, avisar.
Ninguno de los veintiséis es difícil. Lo difícil es acordarse de los veintiséis cuando lo único que quieres es que la pantalla haga por fin lo que le pediste, que es exactamente el estado mental en el que se publican las aplicaciones. Por eso no lo dejes en manos de la memoria: arriba del artículo tienes un botón que te copia el prompt con todo esto, pégaselo a tu agente y que te repase el proyecto. Escribe el código en segundos y lo revisa en segundos; lo que no hace es acordarse solo. Y aunque solo hagas los cinco primeros puntos, ya estás por delante de la mayoría de lo que se publica hoy.
No te lo aprendas: pégaselo a tu agente
Veintiséis puntos no se recuerdan, y menos a las once de la noche con ganas de publicar. Por eso está el botón de «Copiar el prompt», arriba del artículo y aquí mismo: es esta lista entera convertida en un encargo que tu agente entiende.
Lo pegas en Claude Code, en Cursor o en el que uses, y te devuelve un informe punto por punto, con el fichero y la línea, ordenado de más grave a menos y sin tocar nada hasta que tú lo digas. Diez minutos. Y lo que salga en rojo ya sabes dónde mirarlo, porque está más arriba.