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

Lo que pasa cuando alguien escribe tu dominio Quien sea Un lector o un robot Cloudflare Filtra, limita y cachea Tu servidor Solo ve lo que pasó el filtro TODO EL TRÁFICO LO QUE QUEDA Tu IP deja de ser pública. Ya no se puede rodear el filtro llamando a la máquina.
Sin nada delante, todo el mundo llama a tu servidor. Con proxy, primero pasan por la puerta

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

El repaso de dos minutos Certificado válido y renovándose solo HTTP redirige a HTTPS con un 301 Cabecera HSTS con max-age de un año Cookies con Secure, HttpOnly y SameSite Contenido mezclado: una imagen o un script por HTTP La cookie de sesión viaja en la primera petición. Si esa va en claro, da igual el resto.
Lo que hay que dejar puesto y lo que hay que quitar

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

ANTES .env con la contraseña git add . Repositorio público DESPUÉS .env en .gitignore .env.example sin valores Las claves, en el panel Literalmente dos líneas. Es el arreglo más barato de toda la lista.
Dos líneas de diferencia entre las dos filas

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

La clave en el código Funciona a la primera y se publica con el resto sk_live_51H... Entre comillas, en un fichero .ts Viaja en cada commit Y en cada copia del repositorio Cambiarla es tocar código Y volver a desplegar La clave fuera El código solo sabe cómo se llama process.env.STRIPE_SECRET_KEY Un nombre, no un secreto Vive en el panel del hosting Distinta en pruebas y en producción Rotarla es un campo de texto Sin tocar una línea Si le pides a la IA que conecte un servicio, dile también dónde va la clave.
La misma llamada, con la clave dentro y con la clave fuera

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

Lo que pasa de verdad cuando borras una clave Commit con la clave Sin darte cuenta Borras la línea Y haces otro commit Sigue en el historial Cualquiera la recupera Rotas la clave Ahora sí se acabó MEDIA HORA DESPUÉS EL BORRADO ES OTRA VERSIÓN EL ÚNICO ARREGLO REAL Limpiar el historial viene después. Primero se rota, siempre.
Borrar la línea no borra nada: es una versión más encima

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

Dónde acaban los secretos sin querer 01 Registros de compilación Un console.log de la configuración 02 Trazas de error El objeto entero, con sus claves 03 Avisos de integración continua Variables impresas al fallar 04 Capturas en Slack La terminal con el .env abierto Busca las cuatro primeras letras de una clave tuya en los registros. Es un minuto.
Cuatro sitios donde un secreto acaba escrito sin que nadie lo escriba

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

De qué lado va cada cosa La clave publicable de Stripe, en el navegador La clave secreta de Stripe, en el navegador NEXT_PUBLIC_OPENAI_API_KEY, en cualquier sitio La clave de OpenAI, solo en tu servidor El navegador llama a tu API y tu API llama a OpenAI Minificado no es cifrado. Es el mismo texto con los nombres más cortos.
La misma clave según de qué lado de la línea esté

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

Con depuración El error cuenta más de la cuenta La traza entera Con las rutas de tu servidor La consulta que falló Con nombres de tablas y columnas La versión de cada librería Y sus fallos conocidos Sin depuración El usuario ve poco y tú lo ves todo «Algo ha ido mal» Y un identificador del error El detalle, en tus registros Donde solo entras tú Nada de versiones Ni de rutas de tu máquina En la mayoría de frameworks es una variable de entorno. Compruébala hoy.
El mismo error, contado al usuario y contado a tus registros

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

El cementerio de toda aplicación con prisa 01 /test y /debug Datos de ejemplo, sin sesión 02 /admin/tools Para aquella migración de marzo 03 El panel de la base de datos Abierto «solo un rato» 04 El endpoint de seed Que borra y rellena tablas Bórralas, no las protejas. Lo que no existe no se puede olvidar de proteger.
Lo que se monta para salir del paso y nadie vuelve a mirar

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 cinco, de la más fácil a la que da guerra 01 Strict-Transport-Security Nunca más en claro 02 X-Content-Type-Options: nosniff No adivines el tipo 03 X-Frame-Options: DENY Nada de iframes 04 Referrer-Policy Mis URLs no salen 05 Content-Security-Policy De dónde puede cargar Las cuatro primeras son copiar y pegar. La quinta, empiézala en modo aviso.
Cada cabecera cierra una forma distinta de usar tu web contra tu usuario

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

La configuración que se copia de Stack Overflow origin: "*" junto con credentials: true Reflejar el origen que venga en la petición Una lista con tus dos o tres dominios Solo los métodos que usas de verdad En local, el dominio de local. No el asterisco Abierto y con credenciales a la vez es la combinación que duele.
Quién puede llamar a tu API desde el navegador de otra persona

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 de verdad No son dos campos Contraseñas cifradas Sesiones que caducan Límite de intentos Recuperar contraseña Verificar el correo Segundo factor Seis problemas resueltos por otro en dos horas, o mal resueltos por ti en dos semanas.
Todo lo que hay detrás de dos campos y un botón

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 Tu formulario, pagando la factura de otro 360.000 intentos por hora Desde un portátil cualquiera Con la lista de siempre La de contraseñas filtradas Y te enteras por la factura Si te enteras Con límite Cinco clics arriba y diez líneas abajo 5 por minuto y por IP Una regla en Cloudflare Y 5 por minuto y por cuenta Contra el ataque repartido También al recuperar clave Y en los códigos por correo Por IP frena el ruido. Por cuenta frena al que va a por ti.
Cuántas contraseñas se prueban en una hora según lo que dejes

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

CLIENTE Se oculta el botón No se pinta el menú La API acepta igual SERVIDOR Se oculta el botón igual La API pregunta quién eres Y responde 403 si no toca La interfaz es para el usuario legítimo. El servidor es para todos los demás.
Ocultar el botón no impide la petición; el servidor sí

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

Una petición, de fuera hacia dentro 01 Llega una petición A cualquier ruta 02 ¿Está en la lista de públicas? Inicio, precios y login 03 Si no lo está, ¿hay sesión? Si no la hay, fuera 04 ¿Tiene el rol que hace falta? Admin no es usuario 05 Ahora sí, la ruta Y llega ya comprobada La lista corta es la de las abiertas. Nunca la de las cerradas.
El orden en el que se atiende una petición cuando todo nace cerrado

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

Cómo se atiende bien un GET /facturas/1042 1 Llega la petición El 1042 lo escribe quien pide, no tú 2 Se mira quién pregunta De la sesión, jamás de la URL 3 Se busca la factura de ese usuario WHERE id = 1042 AND usuario = sesión 4 Si no hay resultado, 404 Un 403 confirmaría que existe El identificador viene de fuera. El permiso, nunca.
El identificador viene del usuario; el permiso sale de la sesión

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

Tabla abierta La clave del navegador puede con todo SELECT * FROM pedidos Todos, de todo el mundo UPDATE de cualquier fila También las que no son suyas Y nada de esto es un fallo Es la configuración de fábrica Tabla con reglas La regla vive en la base de datos, no en tu código Solo las filas del usuario La base de datos filtra sola Escribir, solo lo propio Lo demás lo rechaza ella Da igual desde dónde pidas La regla es la misma Row Level Security viene apagada. Encenderla es tu trabajo, tabla por tabla.
La misma clave del navegador, con la tabla abierta y con la tabla con reglas

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

Lo que hay que mirar en el almacenamiento El bucket en modo público «para que se vean las imágenes» Nombres de fichero predecibles: /facturas/1042.pdf Bucket privado y enlaces firmados que caducan Nombre de fichero aleatorio, no el id del usuario Reglas que comprueban de quién es el fichero Aquí es donde están los documentos escaneados. Es la carpeta que más duele.
El repaso del almacenamiento, que casi nunca se hace

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

Dónde se comprueba lo que llega La petición Lo que sea que mande quien sea El esquema Tipos, tamaños y formato Tu código Que ya solo ve datos válidos TEXTO SIN COMPROBAR O UN 400 Y AQUÍ ACABA Una cantidad de menos tres, en una tienda, es un abono. Y compila igual de bien.
Si el esquema no lo aprueba, tu lógica ni se entera de que existió

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

El texto del usuario De un buscador, de un formulario Concatenado La base de datos lee una orden Parametrizado La base de datos lee un texto El ORM lo hace solo. El riesgo es la consulta que alguien escribió a mano.
El mismo texto, leído como una orden o leído como un dato

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

El filtro sin mirar Lo que llega entra tal cual en la consulta Se espera un texto Y llega un objeto El objeto es un operador «Distinto de nada» Cierto para cualquier clave Y entra sin saberla El filtro con esquema Primero el tipo, después la consulta Se espera un texto Y se exige un texto Llega un objeto: 400 Antes de tocar la base de datos La consulta no se entera De que existió la petición No hay comillas raras ni nada que parezca un ataque. Es JSON perfectamente válido.
El mismo problema con otra cara: aquí no se cuela una orden, se cuela un operador

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

El orden en el que se acepta un fichero 1 Límite de tamaño Antes de leer nada más 2 Tipo real, leído del contenido No de la extensión del nombre 3 Nombre nuevo, generado por ti El del usuario no se usa jamás 4 A un bucket privado No a la carpeta pública de la web 5 Servido desde otro dominio Para que no herede tus cookies «Solo acepto imágenes» comprobado en el navegador no está comprobado en ningún sitio.
Cinco comprobaciones antes de que un fichero ajeno toque tu disco

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

La vida de un fallo en una librería Aparece el fallo En algo que usas Se publica Con su número Lo prueba cualquiera En diez segundos O ya actualizaste Y no pasa nada EL MISMO DÍA ES PÚBLICO A PROPÓSITO NPM AUDIT Y DEPENDABOT Cuatro mil paquetes, de los que elegiste ocho. Los otros también son tuyos.
El fallo no lo escribes tú: lo instalas y lo arrastras

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

Lo que separa una copia de una carpeta grande 1 Automática y diaria No cuando te acuerdas 2 En otra cuenta y otro sitio Si entran en la tuya, siguen ahí 3 Restaurada una vez, entera En un proyecto de pruebas 4 Y con la fecha apuntada Para saber cuándo toca repetirlo Los incidentes que acaban mal no acaban mal por lo que se llevaron. Por lo que se borró.
Una copia sin probar es una carpeta grande, no una copia

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

Tu correo Recupera todo lo demás GitHub Vercel o Railway Supabase o Firebase El dominio Stripe El panel de correo El segundo factor del correo es el que casi nadie pone. Y abre todos los demás.
Todo lo que se abre con una sola contraseña tuya

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

Las primeras horas, en orden 1 Rota todas las claves Todas, no las que crees que vieron 2 Cierra todas las sesiones Y obliga a volver a entrar 3 Tapa por donde entraron Si ya sabes por dónde fue 4 Mira los registros Ahora sí, con calma 5 Avisa si hay datos de personas En Europa, 72 horas 6 Escribe qué pasó y qué cambiaste La única parte que enseña algo Esconderlo sale infinitamente más caro que contarlo. Siempre.
El orden importa: primero se cierra, después se mira

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.