¿Qué protegen realmente los encabezados de seguridad HTTP importantes (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)?
Cada encabezado de seguridad cierra un ataque específico — clickjacking, MIME sniffing, ataques de downgrade, scripts inyectados — ninguno sustituye arreglar la vulnerabilidad real.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué protegen realmente los encabezados de seguridad HTTP importantes (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Los encabezados de seguridad son defensa en profundidad, no un sustituto de código seguro
Ninguno de los encabezados de abajo arregla una vulnerabilidad en tu aplicación — le indican al navegador que se comporte de forma más defensiva una vez que la respuesta le llega. Son valiosos porque los navegadores son complicados, y buena parte de la explotación real ocurre en el espacio entre "el servidor envió algo razonable" y "el navegador hizo algo que el desarrollador nunca tuvo intención de que hiciera con eso". Cada encabezado cierra un hueco específico en ese espacio.
#Content-Security-Policy (CSP) — controlar qué puede ejecutar y cargar la página
CSP es una lista de permitidos, entregada como encabezado, que le dice al navegador qué orígenes están autorizados para scripts, estilos, imágenes, fuentes, frames y más. Su función principal es limitar el daño de una vulnerabilidad de cross-site scripting (XSS): incluso si un atacante logra inyectar un <script> en una página (a través de un campo de comentarios que el servidor no sanitizó, por ejemplo), una CSP estricta puede impedir que el navegador llegue a ejecutarlo, porque el script inyectado no proviene de un origen permitido y no lleva un nonce válido.
const crypto = require("crypto");const express = require("express");const app = express();app.use((req, res, next) => {const nonce = crypto.randomBytes(16).toString("base64");res.locals.nonce = nonce; // disponible para tu motor de plantillasres.setHeader("Content-Security-Policy",["default-src 'self'",`script-src 'self' 'nonce-${nonce}' https://cdn.example.com`,"style-src 'self' 'unsafe-inline'","img-src 'self' data: https:","frame-ancestors 'none'","object-src 'none'","base-uri 'self'",].join("; "));res.setHeader("Strict-Transport-Security","max-age=31536000; includeSubDomains; preload");res.setHeader("X-Frame-Options", "DENY");res.setHeader("X-Content-Type-Options", "nosniff");res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");next();});
script-src 'self' 'nonce-<random>' es el patrón que vale la pena interiorizar: se genera un nonce aleatorio nuevo por cada petición, se incrusta tanto en el encabezado como en cada etiqueta <script nonce="..."> legítima que el servidor renderiza, y cualquier script que el navegador encuentre sin un nonce coincidente — incluyendo uno inyectado por un atacante — se rehúsa a ejecutar. Recurrir a 'unsafe-inline' en script-src anula la mayor parte del valor de CSP contra XSS, por eso el ejemplo de arriba permite inline sin restricciones solo en style-src, un objetivo de XSS de mucho menor valor que la ejecución de scripts.
Antes de exigir una política nueva en producción, publicala primero como Content-Security-Policy-Report-Only por un tiempo — el navegador reporta las violaciones (a un endpoint report-uri/report-to) sin bloquear nada, que es cómo encontrás el script o la hoja de estilos legítima que tu política habría roto, antes de que la sufran usuarios reales.
#Strict-Transport-Security (HSTS) — eliminar la ventana de downgrade
Sin HSTS, un usuario que escribe example.com (sin esquema) se conecta primero por HTTP plano, y recién es redirigido a HTTPS por la respuesta del servidor — una ventana durante la cual un atacante de red (un punto de acceso Wi-Fi malicioso, por ejemplo) puede interceptar esa primera petición en texto plano y simplemente nunca dejar que ocurra la redirección a HTTPS, manteniendo a la víctima en HTTP indefinidamente mientras hace de proxy hacia el sitio real (un ataque de SSL stripping). Strict-Transport-Security: max-age=31536000; includeSubDomains; preload le dice al navegador: durante el próximo año, nunca vuelvas a intentar HTTP plano para este dominio (ni para ningún subdominio) — actualizá cada petición internamente antes de que salga del navegador, sin ningún round-trip de red que un atacante pueda interceptar.
La directiva preload resuelve la primerísima visita, ese momento en el que el navegador todavía no tiene ningún registro previo de HSTS que hacer cumplir. Los dominios pueden inscribirse en una lista de preload embebida (distribuida dentro de Chrome, Firefox, Safari, etc.) para que la primera conexión que se haga a ellos ya esté forzada a HTTPS, sin ningún hueco de arranque. Inscribirse exige includeSubDomains, un max-age largo, y que HTTPS ya esté sirviendo el encabezado correctamente — es un compromiso de un solo sentido, lento de deshacer si alguna vez necesitás volver a HTTP en un subdominio.
#X-Frame-Options / frame-ancestors — detener el clickjacking
El clickjacking carga tu página dentro de un <iframe> transparente (o disfrazado) en el sitio de un atacante, y luego superpone contenido engañoso para que la víctima crea que está haciendo clic en un botón inofensivo, cuando en realidad está haciendo clic, digamos, en "confirmar transferencia" en tu sitio bancario renderizado de forma invisible debajo. X-Frame-Options: DENY le dice al navegador que se rehúse a renderizar la página dentro de cualquier frame; SAMEORIGIN permite que la enmarquen solo páginas del mismo origen (útil para widgets legítimos del mismo sitio). El equivalente moderno es la directiva de CSP frame-ancestors 'none' (o una lista de orígenes permitidos), que es más expresiva — soporta múltiples orígenes de enmarcado permitidos, donde X-Frame-Options solo soporta uno — pero enviar ambos no cuesta nada y cubre cualquier navegador o proxy que solo entienda el encabezado más antiguo.
#X-Content-Type-Options — detener el abuso del MIME-sniffing
Históricamente los navegadores intentaban adivinar el tipo de contenido real de un recurso inspeccionando sus bytes, incluso cuando el encabezado Content-Type del servidor decía otra cosa — una característica de compatibilidad de la época de servidores mal configurados. El caso de abuso: un atacante sube un archivo a tu sitio con extensión .jpg y un Content-Type de imagen, pero arma sus bytes para que también sean HTML/JavaScript válido; si el navegador lo interpreta como HTML en lugar de confiar en el tipo declarado, y ese archivo se sirve desde tu origen, el atacante consigue ejecución de script en el contexto de tu origen. X-Content-Type-Options: nosniff le dice al navegador: confiá en el Content-Type declarado, no lo cuestiones inspeccionando el contenido — algo que importa especialmente en cualquier endpoint que sirva contenido subido por usuarios desde el mismo origen que tu aplicación autenticada.
#Referencia rápida
| Encabezado | Detiene | No detiene |
|---|---|---|
Content-Security-Policy | La ejecución de scripts y recursos inyectados/no autorizados | El punto de inyección de XSS original en sí — eso es un arreglo a nivel de código |
Strict-Transport-Security | Ataques de downgrade/SSL-stripping, interceptación en texto plano después de la primera visita | La primerísima conexión sin protección, a menos que el dominio esté en la lista de preload |
X-Frame-Options / frame-ancestors | Clickjacking mediante el enmarcado de tu página en otro origen | Ataques que no dependen del enmarcado (por ejemplo, CSRF directo) |
X-Content-Type-Options | Que el MIME-sniffing convierta un archivo de datos en script ejecutable | Servir contenido genuinamente malicioso con el tipo correcto configurado |
#Configuración del servidor, no solo del código de la app
Los mismos encabezados también van en la capa del proxy inverso, tanto como defensa en profundidad como para garantizar que se apliquen incluso a respuestas que tu framework de aplicación no toca (archivos estáticos, páginas de error):
server {listen 443 ssl;server_name app.example.com;add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; object-src 'none'" always;add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;add_header X-Frame-Options "DENY" always;add_header X-Content-Type-Options "nosniff" always;add_header Referrer-Policy "strict-origin-when-cross-origin" always;location / {proxy_pass http://127.0.0.1:3000;}}
La bandera always importa: sin ella, Nginx por defecto solo adjunta add_header a respuestas 2xx/3xx, y descarta silenciosamente estos encabezados en las páginas de error 4xx/5xx — exactamente las respuestas donde un atacante que está sondeando tu app tiene más probabilidades de estar mirando.
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Qué ataque específico resuelve la directiva preload de Strict-Transport-Security que max-age e includeSubDomains por sí solos no resuelven?
2. ¿Por qué usar 'unsafe-inline' en la directiva script-src de una CSP socava su propósito principal?
3. ¿Por qué es significativa la bandera `always` en la directiva add_header de Nginx para encabezados de seguridad?