Skip to content
Volver al blog

Cabeceras de seguridad HTTP: qué configurar y comprobar

CSP con nonce, HSTS y preload, cookies, aislamiento entre orígenes y cabeceras obsoletas: qué configurar, cómo desplegarlo en Nginx, Apache y Cloudflare Pages sin romper el sitio y cómo comprobarlo.

Cada respuesta de tu servidor puede darle instrucciones al navegador: usar solo HTTPS, no ejecutar scripts que no autorizaste, no dejar que otro sitio te muestre en un iframe. Son las cabeceras de seguridad HTTP. Configurarlas cuesta poco, pero una mal puesta puede dejar el sitio en blanco o, en el caso de HSTS, dejar a tus visitantes sin acceso hasta que venza el plazo que fijaste.

Tampoco conviene perseguir una nota. El HTTP Observatory de Mozilla advierte en su propia documentación que sus rangos de notas son "essentially arbitrary", y securityheaders.com explica que su puntaje refleja cuántas cabeceras de seguridad envía tu sitio. Una nota alta no garantiza que estén bien configuradas para tu sitio.

Esta guía revisa qué configurar, qué ya no sirve, cómo desplegar sin romper nada y cómo comprobarlo, según MDN, OWASP, el W3C, el IETF y Chrome, consultados el 4 de noviembre de 2026.

Content-Security-Policy: la que más protege y la que más rompe

CSP define qué código puede ejecutar la página. Sirve sobre todo contra el cross-site scripting (XSS) y también contra el clickjacking, pero OWASP aclara que no debe ser tu única defensa contra XSS.

Política estricta con nonce

Mantener una política con listas de dominios permitidos es trabajoso y, según web.dev, se puede eludir en la mayoría de las configuraciones. web.dev, MDN, OWASP y el W3C recomiendan en cambio la "CSP estricta":

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';
  • El nonce es un valor aleatorio que el servidor genera en cada respuesta y repite en cada <script nonce="..."> legítimo. web.dev pide que sea criptográficamente fuerte, idealmente de 128 bits o más, y nuevo cada vez. Si se puede adivinar, no protege.
  • 'strict-dynamic' deja que un script autorizado cargue otros. Facilita usar herramientas de terceros, aunque la especificación recomienda evitarlo cuando se pueda.
  • https: y 'unsafe-inline' son solo respaldos para navegadores antiguos. Los navegadores que entienden nonces y 'strict-dynamic' los ignoran.
  • object-src 'none' bloquea plugins y base-uri impide inyectar etiquetas <base>. La especificación acepta 'none' o 'self'; usa 'self' si tu sitio tiene su propia etiqueta <base>.

El nonce exige generar HTML en el servidor. Para sitios estáticos o páginas en caché, web.dev recomienda usar hashes ('sha256-...') de cada script en línea. Y antes de activarla, mueve a archivos JavaScript los manejadores como onclick="..." y los enlaces javascript:: una CSP estricta los bloquea.

Prueba primero en modo Report-Only

Content-Security-Policy-Report-Only evalúa la política sin bloquear nada y reporta lo que habría bloqueado. Según MDN, sin la directiva report-to los reportes no se envían. El destino se declara en otra cabecera:

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-reports

report-uri está obsoleta, pero MDN recomienda declarar ambas mientras report-to no funcione en todos los navegadores. La especificación permite enviar a la vez una política aplicada y otra en prueba.

frame-ancestors contra el clickjacking

frame-ancestors 'none' impide que cualquier sitio muestre tu página en un iframe; 'self' lo permite solo a tu propio origen. MDN recomienda 'none' salvo que necesites que tu sitio sea embebible.

Ojo: en una etiqueta <meta> la especificación no acepta frame-ancestors, report-uri, sandbox ni el modo Report-Only. Van como cabecera HTTP.

Strict-Transport-Security y preload

HSTS le indica al navegador que, durante max-age segundos, use solo HTTPS con tu dominio y que no deje al usuario saltarse errores de certificado. Lo define el RFC 6797, de noviembre de 2012, con dos directivas: max-age e includeSubDomains.

  • Solo cuenta por HTTPS. El RFC obliga al navegador a ignorar la cabecera si llega por HTTP.
  • La primera visita queda expuesta. Mientras el navegador no haya recibido la cabecera, una primera petición por HTTP puede ser interceptada. La lista de precarga existe para cerrar esa brecha.
  • includeSubDomains alcanza a todos los subdominios, también a los antiguos que aún no tengan HTTPS.

La directiva preload no es parte del RFC. La lista se gestiona en hstspreload.org, un servicio alojado por Google, y según MDN la usan todos los navegadores. Para entrar, el dominio debe servir HTTPS en todos sus subdominios, incluido www si existe, y enviar max-age de al menos 31536000 segundos (un año) con includeSubDomains y preload.

El riesgo está en salir de la lista. hstspreload.org advierte que la inclusión no se puede deshacer fácilmente: la eliminación puede tardar de 6 a 12 semanas en llegar a la mayoría de los usuarios de Chrome, y más en otros navegadores. También pide no incluir preload por defecto en plantillas: no lo copies de un ejemplo.

Además, hstspreload.org recomienda HSTS, pero no recomienda la precarga: su beneficio adicional es mínimo frente al de HSTS. Considérala solo si aceptas que salir de la lista toma meses.

El despliegue que propone hstspreload.org sube max-age por etapas: 300 segundos (5 minutos), 604800 (una semana) y 2592000 (un mes), esperando el plazo completo de cada etapa antes de pasar a la siguiente. Si algo falla, max-age=0 desactiva HSTS, pero solo en los navegadores que vuelvan a visitarte por HTTPS.

Tres cabeceras simples

  • X-Content-Type-Options: nosniff hace que el navegador respete el Content-Type declarado. Bloquea scripts y hojas de estilo que lleguen con otro tipo, así que revisa antes que tu servidor los declare bien.
  • Referrer-Policy: strict-origin-when-cross-origin envía la URL completa dentro de tu sitio, solo el origen a otros sitios con HTTPS y nada al pasar a HTTP. Ya es el valor por defecto de los navegadores, pero OWASP recomienda declararlo. Si tus URL llevan datos sensibles, no-referrer no envía nada.
  • Permissions-Policy desactiva funciones que no usas, como cámara, micrófono o geolocalización, también dentro de iframes de terceros. MDN la marca como experimental y con soporte parcial.

Aislamiento entre orígenes: COOP, CORP y COEP

  • Cross-Origin-Opener-Policy: same-origin pone tu página en un grupo de navegación separado de las ventanas de otros orígenes, que pierden la referencia mediante window.opener. Si usas ventanas emergentes de inicio de sesión o de pago de terceros, MDN indica same-origin-allow-popups para conservar esa referencia.
  • Cross-Origin-Resource-Policy dice quién puede incrustar tus recursos. web.dev recomienda enviarla en todos, porque un atacante puede cargarlos desde otro sitio para inferir información. OWASP sugiere same-site; usa cross-origin en recursos que otros sitios deban cargar.
  • Cross-Origin-Embedder-Policy: require-corp solo hace falta si necesitas aislamiento entre orígenes, por ejemplo para usar SharedArrayBuffer. OWASP advierte que bloquea los recursos de otros orígenes mal configurados. Si no la necesitas, no la actives.

Cookies

Las cookies de sesión se protegen en la propia Set-Cookie:

Set-Cookie: __Host-sesion=VALOR; Path=/; Secure; HttpOnly; SameSite=Lax
  • Secure: la cookie solo viaja por HTTPS.
  • HttpOnly: JavaScript no puede leerla, lo que reduce el impacto de un XSS. Se sigue enviando en las peticiones del sitio.
  • SameSite: Strict la envía solo en peticiones de tu propio sitio; Lax también en algunas navegaciones desde otros sitios; None en todas y exige Secure. Algunos navegadores aplican Lax si no la declaras, así que declárala.
  • Prefijo __Host-: el navegador solo acepta la cookie si tiene Secure, viene de una página HTTPS, no lleva Domain y usa Path=/. Sin Domain, la cookie se devuelve solo al host que la creó, no a sus subdominios.

Lo que ya no conviene enviar

  • X-XSS-Protection: MDN la marca como obsoleta y advierte que en algunos casos crea vulnerabilidades XSS. OWASP recomienda no enviarla o enviarla en 0, y usar CSP.
  • X-Frame-Options: no está obsoleta, pero frame-ancestors la reemplaza en los navegadores que la soportan. Si envías ambas y frame-ancestors está en una política aplicada (no en Report-Only), la especificación CSP indica que se ignora X-Frame-Options. Puedes mantener DENY para navegadores antiguos; ALLOW-FROM ya no funciona.
  • Expect-CT: según MDN está prácticamente obsoleta desde junio de 2021, y Chromium la declaró obsoleta desde la versión 107 porque ya exige Certificate Transparency por defecto. OWASP: no la uses.
  • Public-Key-Pins: Chromium la eliminó en 2018 y ningún navegador moderno la soporta, según OWASP. Quítala si aún está.
  • Server y X-Powered-By con versiones: OWASP recomienda quitar X-Powered-By y dejar Server sin datos útiles, recordando que hay otras formas de identificar tu tecnología.

Cómo desplegarlas sin romper el sitio

  1. Haz inventario de scripts, iframes y ventanas emergentes de terceros.
  2. Empieza por lo que rara vez rompe algo: nosniff, Referrer-Policy, Permissions-Policy y frame-ancestors.
  3. Deja la CSP de scripts en Report-Only y revisa los reportes antes de aplicarla.
  4. Sube HSTS por etapas; preload es opcional y no se recomienda por defecto.
  5. Prueba errores y redirecciones, no solo la portada.

Nginx

add_header solo actúa en algunos códigos de respuesta (200, 201, 204, 206, 301, 302, 303, 304, 307 y 308) salvo que agregues always. Además, un bloque que define su propio add_header deja de heredar los del nivel superior. Desde la versión 1.29.3, la directiva add_header_inherit permite cambiar esa regla.

server {
    listen 443 ssl;
    server_name example.com;
    server_tokens off;  # no muestra la versión de nginx

    add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;

    location /descargas/ {
        # Este add_header anula la herencia: repite aquí las cabeceras anteriores
        add_header Cache-Control "no-store" always;
    }
}

Apache

Con mod_headers, Header set (por defecto onsuccess) y Header always set usan tablas distintas; solo always se aplica a errores y redirecciones generadas por Apache. Para no duplicar cabeceras, OWASP sugiere quitar la versión onsuccess y fijar con always.

<VirtualHost *:443>
    ServerName example.com
    <IfModule mod_headers.c>
        Header always set Strict-Transport-Security "max-age=300; includeSubDomains"
        Header always set X-Content-Type-Options "nosniff"
        Header always set Referrer-Policy "strict-origin-when-cross-origin"
        Header always set Content-Security-Policy "object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
        # X-Powered-By puede quedar en cualquiera de las dos tablas
        Header unset X-Powered-By
        Header always unset X-Powered-By
    </IfModule>
</VirtualHost>
# En la configuración global: Server solo dice "Apache"
ServerTokens Prod

Cloudflare Pages

El archivo _headers, sin extensión, va en la carpeta de archivos estáticos que se publica; sin framework, directamente en la de salida del build. Admite hasta 100 reglas y 2.000 caracteres por línea, un límite a vigilar con políticas CSP largas. Sus cabeceras no se aplican a las respuestas de Pages Functions: esas se fijan en el código.

/*
  Strict-Transport-Security: max-age=300; includeSubDomains
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Content-Security-Policy: object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Cómo comprobarlas

DevTools de Chrome. En Network, selecciona la petición y abre Headers: Response Headers muestra lo que llegó, y ahí puedes sobrescribir una cabecera localmente para probarla antes de desplegarla. El panel Issues reporta violaciones de CSP y problemas de cookies y de COEP.

curl. curl -I pide solo cabeceras con el método HEAD. Para ver las de un GET, en la portada, en una página de error y por HTTP:

curl -s -D - -o /dev/null https://example.com/
curl -s -D - -o /dev/null https://example.com/no-existe
curl -sI http://example.com/   # debe redirigir a HTTPS, sin HSTS

HTTP Observatory. Está en developer.mozilla.org/en-US/observatory. Se lanzó en MDN el 2 de julio de 2024 y la antigua dirección observatory.mozilla.org redirige ahí. El historial de escaneos de cada dominio es público y está pensado para sitios, no para API.

securityheaders.com. Sigue en funcionamiento; lo creó Scott Helme y hoy es parte de la empresa Snyk. Califica de A+ a F según cuántas cabeceras de seguridad envías; una R indica que la respuesta fue una redirección.

Ninguna nota dice si tu CSP protege de verdad: eso lo dicen los reportes.

Configuración base comentada

Para un sitio que no se embebe en otros ni necesita aislamiento entre orígenes. Ajústala a tu caso:

# HTTPS por un año, sin preload. Llega aquí después de las etapas de prueba
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Aplicada desde el inicio: sin plugins, sin <base> ajeno y nadie (ni tu propio sitio) puede mostrar tus páginas en un iframe
Content-Security-Policy: object-src 'none'; base-uri 'self'; frame-ancestors 'none'

# En prueba: la política estricta de scripts, con reportes
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-reports

# El navegador respeta el Content-Type declarado
X-Content-Type-Options: nosniff

# Solo el origen hacia otros sitios
Referrer-Policy: strict-origin-when-cross-origin

# Funciones que el sitio no usa
Permissions-Policy: camera=(), microphone=(), geolocation=()

# Ventana separada de otros orígenes (same-origin-allow-popups si usas ventanas de pago u OAuth)
Cross-Origin-Opener-Policy: same-origin

# Tus recursos, solo para tu sitio y subdominios
Cross-Origin-Resource-Policy: same-site

# Solo para navegadores antiguos; frame-ancestors tiene prioridad
X-Frame-Options: DENY

# Cookie de sesión
Set-Cookie: __Host-sesion=VALOR; Path=/; Secure; HttpOnly; SameSite=Lax

Lista de verificación

Revisión

Cabecera

Presentes también en errores y redirecciones (always)

Todas

En Nginx, cada location con add_header propio las repite

Todas

CSP estricta probada en Report-Only, sin onclick ni javascript:

CSP

frame-ancestors como cabecera, no en <meta>

CSP

max-age subido por etapas y todos los subdominios con HTTPS

HSTS

preload solo si aceptas que salir toma meses

HSTS

Content-Type correcto en scripts y estilos

nosniff

Sesión con Secure, HttpOnly, SameSite y __Host-

Cookies

Sin X-XSS-Protection activa, Expect-CT ni Public-Key-Pins

Obsoletas

Server y X-Powered-By sin versiones

Todas

DevTools, curl y una herramienta pública tras cada cambio

Todas

Fuentes


Si quieres revisar las cabeceras de tu sitio y planificar cómo activarlas sin interrumpirlo, conversemos.

Comparte este post
Enviar por email

Sigue leyendo

Desarrollo Web

WCAG 2.2: errores de accesibilidad y cómo corregirlos

10 min
Desarrollo Web

Core Web Vitals: cómo medirlos y qué corregir primero

10 min