Bastionado

Cómo configurar las cabeceras de seguridad HTTP en WordPress: CSP, HSTS y las demás

Cabeceras de seguridad HTTP para WordPress, CSP y HSTS

Las cabeceras de seguridad HTTP son unas pocas líneas que se envían con cada página. No reparan un plugin vulnerable, pero piden al navegador de tus visitantes que rechace ciertos comportamientos peligrosos: cargar en HTTP, mostrarse dentro de un marco trampa o ejecutar un script llegado de otro sitio. Este artículo explica qué hace cada cabecera, qué envía ya WordPress, unos valores de partida razonables y el método para probarlas sin romper la administración.

Qué protegen estas cabeceras y qué no

Una cabecera de seguridad es una instrucción: el servidor la envía y el navegador la aplica. Por tanto, la protección recae sobre el visitante y sobre la forma en que se muestran tus páginas. Un bot que ataca directamente wp-login.php o un plugin ignora por completo estas instrucciones.

  • Limitan el impacto de una vulnerabilidad XSS, de un secuestro de clics (clickjacking) o de una interceptación en una wifi pública.
  • No bloquean una inyección SQL, la subida de un archivo malicioso ni una escalada de privilegios en un plugin: solo la actualización corrige esos fallos.
  • Se suman a las demás capas (actualizaciones, autenticación en dos pasos, copias de seguridad), sin sustituirlas.
Esquema que asocia seis cabeceras de seguridad HTTP a los riesgos que limitan, con un indicador verde o naranja según el riesgo de romper algo
Las seis cabeceras útiles y el riesgo que reduce cada una. Las marcadas en naranja requieren una fase de pruebas.

Lo que WordPress ya envía

WordPress añade algunas cabeceras por su cuenta, pero solo en determinadas respuestas. Conocerlas evita duplicarlas con valores contradictorios.

RespuestaCabecera enviada por WordPressDesde
Administración (admin_init) y página de acceso (login_init)X-Frame-Options: SAMEORIGINWordPress 3.1.3
Administración y página de accesoContent-Security-Policy: frame-ancestors 'self';WordPress 6.9
Administración y página de accesoReferrer-Policy: strict-origin-when-cross-originWordPress 4.9 (valor actual desde la 4.9.5)
API REST y admin-ajax.phpX-Content-Type-Options: nosniffPresente en el código actual
Páginas públicas del sitioNinguna de estas cabeceras por defecto
Funciones send_frame_options_header() y wp_admin_headers() del núcleo, comprobadas en el código fuente de WordPress 6.9.

Dicho de otro modo, son tus páginas públicas las que quedan al descubierto. Ahí es donde hay que actuar.

Las seis cabeceras que debes conocer, con valores de partida

CabeceraFunciónValor de partidaRiesgo de romper algo
Strict-Transport-SecurityImpone HTTPS en las visitas siguientesmax-age=31536000 tras un breve periodo de pruebaBajo si todo el sitio está en HTTPS, alto si algún subdominio no lo está
X-Content-Type-OptionsImpide que el navegador adivine el tipo de un archivonosniffMuy bajo
X-Frame-OptionsImpide que otro sitio muestre el tuyo dentro de un marcoSAMEORIGINBajo; DENY rompería la vista previa del personalizador
Referrer-PolicyLimita la URL que se transmite a los sitios enlazadosstrict-origin-when-cross-originMuy bajo
Permissions-PolicyDesactiva las API sensibles del navegadorcamera=(), microphone=(), geolocation=()Medio si un plugin usa la geolocalización o la cámara
Content-Security-PolicyEnumera los orígenes autorizados (scripts, estilos, marcos…)Primero en modo solo informeAlto: hay que construirla sitio por sitio

HSTS: potente, pero difícil de revertir

Strict-Transport-Security indica al navegador que use solo HTTPS durante max-age segundos. El navegador ignora esta cabecera si llega por HTTP, y la primerísima visita sigue expuesta mientras no la haya recibido nunca. Para desactivar HSTS hay que enviar max-age=0 por HTTPS: imposible si tu certificado ha caducado entretanto.

Secuencia HSTS entre un navegador y un sitio WordPress: petición HTTP, redirección 301, petición HTTPS y respuesta con la cabecera Strict-Transport-Security
Solo la primera petición en HTTP queda expuesta. Las visitas siguientes van directamente por HTTPS.

La lista de precarga hstspreload.org elimina ese punto débil, con condiciones estrictas: certificado válido, redirección de HTTP a HTTPS en el mismo host, todos los subdominios en HTTPS y una cabecera con max-age de al menos 31536000 segundos (un año), includeSubDomains y preload. El sitio advierte de que la inscripción no se deshace fácilmente y de que la retirada tarda meses en llegar a los usuarios. Solicita la precarga solo si todos tus subdominios (correo web, preproducción, antigua extranet) van a estar en HTTPS durante mucho tiempo.

X-Frame-Options y frame-ancestors

Ambas impiden que otro sitio muestre el tuyo en un marco invisible para hacerte hacer clic sin que lo sepas. OWASP recomienda la directiva CSP frame-ancestors, que sustituye a X-Frame-Options en los navegadores que la admiten. Enviar las dos con SAMEORIGIN y 'self' reproduce lo que WordPress ya hace en la administración desde la versión 6.9.

Content-Security-Policy: empezar en modo informe

La CSP es la cabecera más eficaz contra los scripts inyectados y la más delicada en WordPress: el núcleo, los temas y muchos plugins escriben scripts en línea. Una política estricta copiada de un tutorial suele bloquear el editor, los formularios o las estadísticas. El método correcto:

  1. Envía Content-Security-Policy-Report-Only: el navegador informa de las infracciones sin bloquear nada.
  2. Abre la consola de las herramientas de desarrollo en la página de inicio, una entrada, el carrito y el editor, y anota cada origen bloqueado.
  3. Para recoger los informes a distancia, añade report-to y la cabecera Reporting-Endpoints: sin destino, no se envía ningún informe.
  4. Pasa a Content-Security-Policy solo cuando la consola quede limpia en todos los recorridos importantes.

Desde WordPress 5.7, las funciones wp_get_script_tag(), wp_get_inline_script_tag() y el filtro wp_script_attributes permiten añadir un atributo nonce a los scripts cargados correctamente. Los scripts escritos directamente en el HTML por un tema o un plugin no se benefician de ello: por eso muchas políticas para WordPress conservan 'unsafe-inline' al principio, lo que debilita la protección contra XSS.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'

Este punto de partida es permisivo a propósito: sirve para medir, no para proteger. Después ve ajustándolo origen por origen (dominio de las fuentes tipográficas, herramienta de estadísticas, pasarela de pago).

Las cabeceras que ya no hay que añadir

Muchas guías antiguas y muchos plugins siguen proponiendo cabeceras abandonadas. Añadirlas ya no protege nada, y una de ellas incluso puede ser perjudicial.

CabeceraEstado actualQué hacer
X-XSS-ProtectionObsoleta y no estándar (MDN). Puede crear vulnerabilidades XSS en sitios sanos.No enviarla, o enviarla con valor 0. Usar una CSP.
Expect-CTDesaconsejada por OWASP, que recomienda quitarla.Eliminarla.
Feature-PolicySustituida por Permissions-Policy, con una sintaxis distinta.Convertirla, por ejemplo camera=().
X-Powered-By y Server detalladaNo son cabeceras de seguridad: anuncian la versión de PHP o del servidor.Quitarlas (expose_php = Off en PHP) o neutralizarlas.

Dónde configurarlas: servidor, PHP o plugin

La elección depende de tu alojamiento. La regla general: cuanto antes se aplique la instrucción, más respuestas cubre.

  • En el servidor (.htaccess de Apache o LiteSpeed, configuración de Nginx): cubre también los archivos estáticos, los errores y las páginas que sirve una caché de páginas sin pasar por PHP.
  • En PHP (acción send_headers): práctico en un alojamiento sin acceso a la configuración, pero no aparece en las respuestas que no cargan WordPress, como una página servida directamente por una caché.
  • Con un plugin: la misma limitación que PHP, con una interfaz. Comprueba que no añade las cabeceras obsoletas vistas más arriba.

Apache o LiteSpeed (.htaccess)

Con mod_headers, la condición always añade la cabecera también en las respuestas de error y las redirecciones, cosa que no hace la condición por defecto.

<IfModule mod_headers.c>
  Header always set Strict-Transport-Security "max-age=31536000"
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "SAMEORIGIN"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

Nginx

Sin el parámetro always (disponible desde Nginx 1.7.5), add_header solo se aplica a los códigos 200, 201, 204, 206, 301, 302, 303, 304, 307 y 308. Otra trampa: un bloque location que define su propio add_header deja de heredar las cabeceras del nivel server. Nginx 1.29.3 añadió add_header_inherit merge; para cambiar este comportamiento.

add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

En PHP, en un plugin propio

add_action( 'send_headers', function () {
    header( 'X-Content-Type-Options: nosniff' );
    header( 'Referrer-Policy: strict-origin-when-cross-origin' );
    header( 'Permissions-Policy: camera=(), microphone=(), geolocation=()' );
} );

No añadas HSTS en PHP si tu proveedor de alojamiento o tu CDN ya lo envía: dos valores distintos complican el diagnóstico.

Probar sin romper el sitio

  1. En la línea de comandos: curl -I https://tu-sitio.es/ muestra las cabeceras que realmente se reciben. Prueba también una página 404, una imagen de wp-content/uploads y wp-login.php.
  2. En el navegador: pestaña Red de las herramientas de desarrollo y, después, la respuesta del documento principal.
  3. Con el HTTP Observatory de Mozilla, integrado en MDN desde julio de 2024, que puntúa la configuración y explica cada punto.
  4. Recorre después las funciones sensibles: editor de bloques, personalizador, formularios, pago, vídeos incrustados.
  5. Vacía todas las cachés (plugin, alojamiento, CDN) antes de cada prueba: una página en caché puede conservar las cabeceras antiguas.
Conferencia en francés: panorama de las cabeceras de seguridad (CSP, HSTS, X-Frame-Options, COxP) y de los ataques que limitan.
Conferencia de AFUP dedicada a implantar una Content Security Policy en una aplicación PHP (en francés).

Con WP Masquerade

El plugin gratuito ya añade X-Content-Type-Options: nosniff desde el nivel 1 del módulo de bastionado. WP Masquerade Pro gestiona las cabeceras de seguridad desde la administración (HSTS, X-Frame-Options, Referrer-Policy, CSP…), sin modificar los archivos del servidor. Como con cualquier solución en PHP, una página servida por una caché previa puede no llevar estas cabeceras: comprueba el resultado con curl y, si hace falta, consulta la página de compatibilidad con alojamientos y cachés. La comparativa detalla la frontera entre la versión gratuita y la Pro.

Una plataforma de consentimiento de cookies alojada carga su script desde su propio dominio, que entonces hay que autorizar en la CSP; nuestra comparativa de plugins de cookies indica qué soluciones se quedan en su servidor.

Lo esencial

  • Las cabeceras de seguridad protegen a tus visitantes, no tu código: complementan las actualizaciones sin sustituirlas.
  • WordPress ya protege la administración y el acceso frente a la visualización en marcos; tus páginas públicas no tienen nada por defecto.
  • Despliega primero nosniff, SAMEORIGIN, Referrer-Policy y Permissions-Policy, que rara vez rompen algo.
  • Sube HSTS por etapas y solicita la precarga solo si todos los subdominios están en HTTPS.
  • Construye la CSP en modo solo informe, página a página, antes de aplicarla.
  • Deja de enviar X-XSS-Protection, Expect-CT y Feature-Policy.

Fuentes

Otras fuentes consultadas: documentación oficial.

Seguridad de WordPress, sin ruido

Un correo cuando importa: nuevos ataques observados, actualizaciones de la extensión y guías prácticas. Sin spam, baja con un clic.

Empieza gratis y pasa a Pro cuando estés listo

La versión gratuita ya oculta WordPress y cambia la dirección de la página de acceso. Pro añade alias, verificación en dos pasos y supervisión.