Endureciendo este sitio: de una D a una A+
Cuando terminé de construir este portfolio, lo pasé por una auditoría automatizada antes de desplegarlo. Uno de los hallazgos se quedó en la pila de "prioridad media" durante un tiempo: el sitio no tenía ninguna cabecera de seguridad. Sin Content-Security-Policy, sin X-Frame-Options, sin Referrer-Policy, sin X-Content-Type-Options, sin Permissions-Policy. Además, el sitio exponía silenciosamente una cabecera X-Powered-By: Next.js, contándole a cualquiera exactamente qué framework y qué versión buscar para encontrar vulnerabilidades conocidas.
Al final me senté a arreglarlo bien. Esto es lo que implicó, con números reales.
Dónde empezó todo
Pasé el sitio en vivo por dos escáneres independientes: securityheaders.com y el HTTP Observatory de Mozilla. Ninguno de los dos es marketing con opinión propia, simplemente comprueban qué se está enviando realmente en la respuesta HTTP.
- securityheaders.com: D
- Mozilla Observatory: C-, 45/100
Ambos señalaron el mismo problema de fondo. Las cabeceras que faltan no son solo una formalidad, cada una cierra una vía de ataque concreta:
- Content-Security-Policy le dice al navegador exactamente qué fuentes tienen permiso para ejecutar scripts, cargar estilos, o pedir datos. Sin ella, si alguien alguna vez encuentra la forma de inyectar un script en la página (a través de un formulario, un campo de comentarios, cualquier sitio donde la entrada del usuario toque el DOM), el navegador lo ejecutará sin más.
- X-Frame-Options evita que otras webs carguen la tuya dentro de un iframe invisible para engañar a los visitantes y que hagan clic en algo que no querían, esto se llama clickjacking.
- X-Content-Type-Options evita que el navegador intente adivinar el tipo de un archivo basándose en su contenido en vez de confiar en el tipo declarado, algo pequeño que cierra toda una categoría de ataques por MIME-sniffing.
- Referrer-Policy controla cuánta información se filtra a otras webs cuando alguien hace clic en un enlace que sale de la tuya.
Averiguar cómo lo encontraría alguien de fuera
Antes de arreglar nada, merece la pena entender cómo descubriría esto realmente alguien externo, porque ese es el modelo de amenaza real, no algo hipotético.
Si tu repositorio de GitHub es público, cualquiera puede ejecutar npm audit o pnpm audit directamente contra tu lockfile y obtener una lista de CVEs conocidas en tus dependencias. Herramientas como Snyk o Socket.dev hacen lo mismo con una interfaz más cuidada. E incluso sin tocar tu código, herramientas como Wappalyzer identifican tu web solo leyendo el HTML y las cabeceras que sirve, si estás filtrando X-Powered-By: Next.js, le acabas de dar a alguien el framework exacto y la versión exacta que buscar.
Ese último punto es justo por qué quitar esa cabecera no era opcional.
Qué implementé exactamente
En next.config.mjs, añadí un bloque headers() que cubre:
- Una Content-Security-Policy ajustada a lo que el sitio usa realmente. No una política genérica copiada de cualquier sitio, repasé el código para identificar cada recurso externo que se carga de verdad (Resend para el formulario de contacto, las fuentes, los chunks que mueven el globo en three.js) y escribí la política alrededor de exactamente eso. La primera versión todavía tenía
unsafe-inliney fuentes demasiado amplias como unhttps:genérico enobject-src, que el Observatory de Mozilla marcó correctamente como "implementada de forma insegura". La ajusté: quitéunsafe-inlineydata:descript-src, y puseobject-srcennoneen vez de dejarlo abierto. - X-Frame-Options: DENY
- Referrer-Policy: strict-origin-when-cross-origin
- X-Content-Type-Options: nosniff
- Permissions-Policy, desactivando APIs del navegador que el sitio no usa (cámara, micrófono, geolocalización)
- Cross-Origin-Resource-Policy: same-origin
poweredByHeader: false, eliminando por completo la huella del framework- El flag Secure en las cookies, señalado aparte por el Observatory
Probé el sitio entero después de cada pasada, ambos idiomas, el formulario de contacto, el globo 3D, porque una CSP demasiado estricta rompe cosas en silencio en vez de lanzar un error evidente. Ese es el riesgo real del hardening: hacer el sitio técnicamente más seguro mientras lo rompes en silencio para los visitantes reales.
Dónde terminó
- securityheaders.com: A+
- Mozilla Observatory: A+
El mismo sitio, la misma funcionalidad, solo que ahora las cabeceras de respuesta hacen su trabajo de verdad. También añadí un campo honeypot y un límite básico de peticiones en la ruta del formulario de contacto, porque un formulario sin protección es un blanco fácil para bots de spam, por muy bien que se vean las cabeceras.
Nada de esto requirió reescribir la aplicación. Es el tipo de pasada que es fácil saltarse porque nada de ella es visualmente obvio, y creo que precisamente por eso merece la pena hacerla bien. Un sitio puede parecer terminado y aun así tener una puerta sin cerrar.