Cuando lanzas una app con Cursor, Lovable o v0, el servidor responde a cada petición con una serie de cabeceras HTTP: instrucciones invisibles que el navegador obedece antes de pintar nada. Un puñado de ellas decide buena parte de la seguridad de tu web, y casi ninguna plantilla las trae activadas. Esta guía explica qué hace cada una, qué valor poner, los errores que rompen cosas y cómo configurarlas en Vercel, Next.js, Nuxt, Netlify, Cloudflare o nginx.
Qué es una cabecera y cómo ver las tuyas
Una cabecera es una línea de texto que acompaña a la respuesta, del tipo Nombre: valor. El navegador la lee y cambia su comportamiento: dejar o no cargar un script, forzar HTTPS, permitir o no que tu página se meta en un iframe. Puedes verlas en las herramientas de desarrollador (F12, pestaña Network, clic en el documento principal, apartado Response Headers) o desde la terminal:
curl -I https://tudominio.com
Si en esa lista no aparecen las de abajo, no están configuradas. No hay valores por defecto que te salven.
Las siete que importan
Content-Security-Policy (CSP)
Es la reina. Le dice al navegador de qué orígenes puede cargar scripts, estilos, imágenes, fuentes y conexiones. Si un atacante logra inyectar un <script> en tu página, una CSP bien puesta impide que se ejecute. Un punto de partida honesto para una app moderna:
Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
El error típico es poner 'unsafe-inline' en script-src porque "si no, no funciona": eso tira por tierra la protección principal. Los frameworks modernos resuelven los scripts inline con un nonce (un valor aleatorio por petición) o con hashes. Y la forma segura de empezar es en modo informe: la cabecera Content-Security-Policy-Report-Only no bloquea nada, solo te cuenta qué bloquearía. Déjala así unos días, revisa los informes y pásala a la cabecera real cuando no rompa nada.
Strict-Transport-Security (HSTS)
Obliga al navegador a usar HTTPS durante el tiempo indicado, aunque el usuario escriba http:// o pulse un enlace antiguo. Evita que alguien en la misma red degrade la conexión. El valor habitual:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Actívala solo cuando todo tu dominio y sus subdominios vayan por HTTPS: si la pones con un subdominio en HTTP, lo dejas inaccesible durante un año en los navegadores que la hayan visto. Empieza con un max-age corto si tienes dudas.
X-Frame-Options y frame-ancestors
Impiden que tu web se cargue dentro de un <iframe> de otra página. Sin esto, alguien puede incrustar tu app en un sitio falso y engañar a los usuarios para que pulsen donde no deben: es el clickjacking. X-Frame-Options: DENY o SAMEORIGIN lo resuelve en navegadores antiguos; la versión moderna es la directiva frame-ancestors dentro de la CSP. Pon las dos.
X-Content-Type-Options
Un solo valor: nosniff. Impide que el navegador adivine el tipo de un archivo y ejecute como script algo que serviste como texto o imagen. Es una línea; no tiene excusa para faltar.
Referrer-Policy
Controla cuánta información de la URL de origen se envía al navegar a otro sitio o cargar un recurso externo. Sin control, puedes filtrar rutas privadas o tokens que viajan en la URL. strict-origin-when-cross-origin es el equilibrio sensato: tu dominio sí, la ruta completa no.
Permissions-Policy
Declara qué capacidades del navegador puede usar tu página y sus iframes: cámara, micrófono, geolocalización, pagos. Si no las usas, desactívalas; así un script inyectado o un iframe de terceros tampoco puede. Por ejemplo: camera=(), microphone=(), geolocation=().
Cross-Origin-Opener-Policy
Menos conocida: aísla tu ventana de las que la abrieron o que abriste con window.open, lo que corta una familia de ataques entre pestañas. same-origin es el valor habitual, salvo que dependas de ventanas emergentes de terceros (algunos logins con OAuth), donde conviene same-origin-allow-popups.
Las que conviene quitar
X-Powered-By y una cabecera Server con versión anuncian tu stack exacto a quien busque fallos conocidos. En Next.js se quita con poweredByHeader: false; en Express, con app.disable('x-powered-by'). Y X-XSS-Protection está obsoleta: los navegadores modernos la ignoran y en los antiguos llegó a causar fallos. No la pongas, o ponla a 0.
Cómo ponerlas según tu hosting
Vercel, para cualquier framework, en vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
Next.js: la función headers() de next.config.js, con la misma lista; la CSP con nonce se genera en el middleware. Nuxt: routeRules en nuxt.config.ts con un objeto headers, o un plugin de servidor si necesitas nonce. Netlify: un archivo _headers en la carpeta publicada:
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Cloudflare: reglas de transformación de cabeceras de respuesta en el panel, sin tocar código. nginx en un VPS:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
La CSP la dejo fuera de estos bloques a propósito: es la única que depende de qué scripts y servicios usa tu app, y conviene construirla en modo informe antes de fijarla.
Errores que rompen cosas
- HSTS antes de tener todo en HTTPS. Deja fuera a quien tenga un subdominio en HTTP. Verifica primero, activa después.
- CSP sin nonce en Next.js o Nuxt. Ambos inyectan scripts inline para hidratar la página; sin nonce, la CSP estricta los bloquea y la app queda en blanco. Usa el mecanismo de nonce del framework.
- Cabeceras solo en el HTML. Las rutas de API y los archivos estáticos también las necesitan, sobre todo
nosniff. Aplica la regla a/(.*). - Cabeceras duplicadas. Si el CDN y la app añaden la misma, el navegador puede quedarse con la más restrictiva o ignorar ambas. Decide un sitio.
- Olvidar los subdominios (app., api., docs.): cada uno responde con sus propias cabeceras.
Compruébalas desde fuera
Estas cabeceras son información pública: cualquiera puede leerlas, incluido tú. El verificador gratuito de cabeceras te lista las que faltan y las que están mal en segundos. Y Scanaris hace la comprobación completa de solo lectura, con unas 45 cosas más, y por cada cabecera ausente te da un fix prompt listo para pegar en tu IA con la línea exacta para tu hosting. No explota nada ni toca tu servidor: lee y te dice qué activar. Si quieres el contexto completo, está en la guía de seguridad para apps hechas con IA y en la checklist de lanzamiento.
Preguntas frecuentes
¿Las cabeceras hacen más lenta la web?
No. Son unos cientos de bytes por respuesta y el navegador las procesa en microsegundos. HSTS incluso la acelera, porque evita la redirección de HTTP a HTTPS en visitas posteriores.
¿Qué pasa si una cabecera rompe algo en producción?
Quítala o relájala y vuelve a desplegar: el efecto es inmediato, salvo HSTS, que los navegadores recuerdan durante el max-age. Por eso la CSP se prueba en modo informe y HSTS se activa al final.
¿Necesito CSP si mi app es pequeña?
Si tu app tiene un formulario o muestra contenido de usuarios, sí: es la defensa que convierte un XSS en un incidente menor. Empieza con el resto de cabeceras, que son triviales, y añade la CSP en modo informe cuando puedas dedicarle una tarde.