Cabeceras de seguridad HTTP:
CSP, HSTS y compañía sin dolor de cabeza
Privacidad y seguridad web | unfantasmaenelsistema.com
Cuando un navegador pide una página, el servidor no solo devuelve el HTML: adjunta una lista de cabeceras HTTP con instrucciones sobre cómo tratar esa respuesta. Un puñado de esas cabeceras existe solo para reforzar la seguridad —le dicen al navegador qué está permitido y qué no—, y activarlas suele ser cuestión de añadir unas pocas líneas a la configuración del servidor. El problema es que sus nombres asustan (Content-Security-Policy, Strict-Transport-Security) y la documentación oficial tiende a ser un muro de directivas.
Este artículo recorre las cabeceras de seguridad que de verdad importan hoy: qué ataque frena cada una, qué valor poner, y cómo configurarlas en Nginx y en Apache sin romper el sitio. Es un complemento natural de nuestro artículo sobre Burp Suite: allí se ven los ataques desde el lado ofensivo, aquí se cierran desde el lado del servidor.
Qué aprenderás en este artículo
Qué hace cada cabecera de seguridad y contra qué protege · Cómo construir una Content-Security-Policy sin bloquear tu propio sitio · Por qué HSTS necesita cuidado (y qué es la lista de precarga) · Los atributos de cookie que convierten una sesión robable en una sesión protegida · Bloques de configuración listos para Nginx y Apache, y cómo verificar el resultado
1. Por qué el navegador necesita que le des permiso
El modelo de seguridad del navegador es permisivo por defecto por motivos históricos: carga scripts de donde le digan, permite que la página se incruste en un iframe ajeno, envía cookies en casi cualquier petición. Muchos ataques web —desde el robo de sesión por XSS hasta el clickjacking— se aprovechan justo de esa permisividad.
Las cabeceras de seguridad son la forma que tiene el servidor de decirle al navegador «para este sitio, endurece las reglas». No sustituyen a programar de forma segura, pero sí actúan como una segunda capa: si un fallo de la aplicación deja colar un script inyectado, una CSP bien puesta puede impedir que ese script llegue a ejecutarse o a filtrar datos.
Regla de oro antes de tocar nada
Aplica estas cabeceras en un entorno de pruebas primero · CSP y HSTS pueden dejar un sitio aparentemente roto (recursos que no cargan, imposibilidad de volver a HTTP) si se configuran a ciegas · Casi todas tienen un modo «solo reportar» o un despliegue gradual: úsalo
2. Tabla de referencia rápida
| Cabecera | Qué ataque frena | Valor recomendado para empezar |
|---|---|---|
| Strict-Transport-Security | Downgrade a HTTP, SSL stripping en la primera visita repetida | max-age=31536000; includeSubDomains |
| Content-Security-Policy | XSS, inyección de recursos, exfiltración de datos | default-src ‘self’; object-src ‘none’; frame-ancestors ‘none’ |
| X-Content-Type-Options | MIME sniffing (que el navegador ejecute como script algo que no lo es) | nosniff |
| X-Frame-Options | Clickjacking (versión heredada, para navegadores viejos) | DENY |
| Referrer-Policy | Fuga de URLs internas y tokens en el encabezado Referer | strict-origin-when-cross-origin |
| Permissions-Policy | Uso no deseado de cámara, micrófono, geolocalización, etc. | camera=(), microphone=(), geolocation=() |
| Cross-Origin-Opener-Policy | Ataques de canal lateral entre ventanas (Spectre), tabnabbing | same-origin |
El resto del artículo desarrolla las que tienen más matices. Las tres últimas de la tabla (X-Content-Type-Options, Referrer-Policy y Permissions-Policy) se pueden poner tal cual y olvidarse; las demás merecen un párrafo.
3. HSTS: obligar a HTTPS de forma permanente
Strict-Transport-Security le dice al navegador «a partir de ahora, para este dominio, conéctate siempre por HTTPS aunque el usuario escriba http:// o siga un enlace inseguro». El navegador recuerda esa instrucción durante el tiempo que marque max-age (en segundos) y ni siquiera intenta la conexión insegura: la reescribe a HTTPS antes de salir.
3.1 Las directivas
- max-age=31536000: cuánto tiempo (aquí, un año) el navegador recuerda la política. Empieza con un valor bajo, como 300, mientras pruebas
- includeSubDomains: aplica la regla a todos los subdominios. No lo añadas hasta estar seguro de que TODOS tus subdominios sirven HTTPS correctamente
- preload: solicita la inclusión en la lista de precarga que los navegadores traen de fábrica
HSTS no se puede deshacer con rapidez
Si activas HSTS con max-age de un año y luego tienes un problema con el certificado, los visitantes que ya recibieron la cabecera NO podrán acceder al sitio hasta que lo arregles: el navegador se niega a conectar por HTTP y no hay forma de saltárselo · La directiva preload es prácticamente permanente: salir de la lista de precarga tarda meses en propagarse. No la uses salvo que HTTPS sea definitivo en todo el dominio y subdominios
4. Content-Security-Policy: la más potente y la más temida
CSP es una lista blanca de orígenes desde los que el navegador tiene permiso para cargar cada tipo de recurso: scripts, hojas de estilo, imágenes, fuentes, marcos, conexiones. Todo lo que no esté en la lista se bloquea. Bien afinada, es la defensa más eficaz contra el XSS: aunque un atacante consiga inyectar una etiqueta <script>, si su código intenta cargarse desde un dominio ajeno o ejecutarse en línea, la CSP lo corta.
4.1 Directivas más habituales
| Directiva | Controla |
|---|---|
| default-src | Valor por defecto para el resto de directivas que no especifiques |
| script-src | De dónde se pueden cargar y ejecutar scripts |
| style-src | Hojas de estilo y estilos en línea |
| img-src | Imágenes |
| connect-src | Destinos de fetch, XHR, WebSocket, EventSource |
| font-src | Fuentes web |
| frame-ancestors | Quién puede incrustar TU página en un iframe (sustituye a X-Frame-Options) |
| object-src | Plugins: <object>, <embed>. Casi siempre debe ser ‘none’ |
| base-uri | Restringe la etiqueta <base>, que un atacante podría usar para redirigir rutas relativas |
4.2 Valores de origen que más se usan
- ‘self’: el mismo origen (esquema + dominio + puerto) que la página
- ‘none’: no se permite ningún origen
- ‘unsafe-inline’: permite scripts y estilos en línea — evítalo en script-src, es la puerta que la CSP intenta cerrar
- ‘nonce-XXXX’: permite solo las etiquetas script que lleven ese número aleatorio de un solo uso, generado por petición
- ‘strict-dynamic’: confía en los scripts cargados por un script ya autorizado; simplifica CSP en sitios con mucho JavaScript
4.3 Despliegue sin romper nada: primero, modo reporte
En lugar de Content-Security-Policy, envía Content-Security-Policy-Report-Only. El navegador no bloquea nada: solo registra en la consola (y, si configuras un destino con report-to, envía a un endpoint) todo lo que la política habría bloqueado. Dejas eso unos días, revisas los informes, ajustas la lista blanca hasta que no haya falsos positivos, y entonces cambias la cabecera al modo que sí aplica.
# Fase 1: observar sin bloquear Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report # Fase 2: politica estricta con nonce (el valor cambia en cada respuesta) Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; \ object-src 'none'; base-uri 'self'; frame-ancestors 'none'
5. Clickjacking: frame-ancestors y X-Frame-Options
El clickjacking consiste en cargar tu sitio en un iframe transparente sobre otra página, de forma que la víctima crea que pulsa un botón inocente cuando en realidad está pulsando algo en tu sitio (donde tiene la sesión iniciada). La defensa es impedir que tu página se incruste donde no quieras.
- Content-Security-Policy: frame-ancestors ‘none’ — método moderno y preferido. Usa ‘self’ si necesitas incrustarte en tu propio dominio, o lista dominios concretos
- X-Frame-Options: DENY — método heredado. Solo entiende DENY, SAMEORIGIN y (mal soportado) ALLOW-FROM. Mantenlo por navegadores antiguos, pero frame-ancestors manda cuando ambos están
6. Cookies: Secure, HttpOnly y SameSite
No son cabeceras de respuesta independientes, sino atributos que se añaden a la cabecera Set-Cookie, y son de lo más rentable que puedes tocar: convierten una cookie de sesión trivialmente robable en una mucho mejor protegida.
| Atributo | Efecto | Cuándo usarlo |
|---|---|---|
| Secure | La cookie solo se envía por HTTPS, nunca en claro | Siempre, en cualquier cookie de un sitio con HTTPS |
| HttpOnly | JavaScript no puede leer la cookie (document.cookie no la ve) | Siempre en cookies de sesión: neutraliza el robo por XSS |
| SameSite=Lax | La cookie no viaja en peticiones cross-site, salvo navegación de nivel superior | Valor por defecto razonable, mitiga CSRF |
| SameSite=Strict | La cookie nunca viaja en peticiones cross-site | Cookies muy sensibles, si no rompe el flujo de login |
| SameSite=None | Se permite cross-site (obliga a Secure) | Solo si de verdad necesitas la cookie en contextos de terceros |
Set-Cookie: session=a1b2c3; Path=/; Secure; HttpOnly; SameSite=Lax
7. Configuración en Nginx y Apache
7.1 Nginx
# En el bloque server { } (o dentro de un include comun)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# El parametro 'always' es importante: sin el, la cabecera no se anade
# en respuestas de error (404, 500), justo donde tambien hace falta.Cuidado con add_header y la herencia en Nginx
Si defines add_header en un bloque hijo (location), Nginx DESCARTA todos los add_header heredados del bloque padre · O centralizas todas las cabeceras en el nivel más alto, o las repites en cada location que use su propio add_header
7.2 Apache
# Requiere: a2enmod headers # En el VirtualHost o en un .htaccess Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "DENY" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
8. Verificar el resultado
- securityheaders.com — escáner rápido de Scott Helme: puntúa de A+ a F y explica cada cabecera ausente
- Mozilla Observatory (observatory.mozilla.org) — análisis más completo, incluye TLS y cookies, con recomendaciones concretas
- csp-evaluator.withgoogle.com — específico para auditar si tu Content-Security-Policy tiene huecos conocidos (dominios que permiten bypass, ‘unsafe-inline’ olvidado)
- curl -I https://tu-sitio — comprobación manual de las cabeceras que devuelve el servidor
curl -sI https://unfantasmaenelsistema.com | grep -iE 'content-security|strict-transport|x-frame|x-content|referrer|permissions'
No persigas la A+ a ciegas
La puntuación de un escáner es una guía, no el objetivo · Una CSP que puntúa alto pero incluye ‘unsafe-inline’ porque no te atreviste a quitarlo protege menos que una CSP intermedia bien entendida · Prioriza: HTTPS + HSTS, cookies con HttpOnly/Secure/SameSite, y una CSP real (aunque empiece siendo modesta) por encima de rellenar cabeceras cosméticas
Conclusión
Las cabeceras de seguridad HTTP son de las mejoras con mejor relación esfuerzo/beneficio en toda la seguridad web: unas líneas en la configuración del servidor y ganas una capa de defensa que actúa incluso cuando la aplicación falla. Las de bajo riesgo —X-Content-Type-Options, Referrer-Policy, Permissions-Policy, los atributos de cookie— se ponen hoy y no se vuelven a tocar.
Las dos que exigen cabeza son HSTS y CSP. HSTS, porque es difícil de revertir: despliégala con max-age corto y ve subiéndolo. CSP, porque puede romper el sitio: empléala primero en modo solo-reporte, lee los informes, y endurécela por fases hasta llegar a una política estricta con nonces. Hecho así, el dolor de cabeza del título no llega a aparecer.
Referencias
MDN Web Docs — referencia de cada cabecera HTTP (developer.mozilla.org) · OWASP Secure Headers Project — owasp.org/www-project-secure-headers · securityheaders.com y observatory.mozilla.org — escáneres online · content-security-policy.com — guía práctica de CSP directiva por directiva · Nuestro artículo sobre Burp Suite, para ver estos ataques desde el lado ofensivo
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

