Contents
Introducción — por qué endurecer cabeceras desde PHP en WordPress
Endurecer las cabeceras de seguridad (Content-Security-Policy, HSTS y otras) reduce la superficie de ataque: previene XSS por ejecución de scripts no autorizados, fuerza HTTPS, evita clickjacking y protege contra ataques por mezcla de contenidos. En WordPress, aplicar estas cabeceras desde PHP tiene la ventaja de ejecutarse en la capa de aplicación (control total sobre cuándo y cómo), integrarse con la generación de páginas y permitir mecanismos dinámicos como nonces CSP para scripts y estilos encolados.
Consideraciones iniciales
- Enviar HSTS sólo sobre HTTPS. Nunca envíes Strict-Transport-Security en conexiones HTTP—puede inhibir el acceso si se fuerza a HTTPS cuando no está configurado correctamente.
- Pruebas progresivas. Empieza con políticas restrictivas en modo reporte (report-only) y solo cuando estés seguro pasa a modo enforcement.
- Entorno WordPress. Ten en cuenta: admin-ajax, REST API (/wp-json), temas y plugins pueden inyectar scripts inline o fuentes externas. Hay que analizarlos y adaptarlos (nonces o hashes).
- Mu-plugin recomendado. Para asegurar que las cabeceras se apliquen siempre (incluso si se desactiva un plugin estándar), usa un mu-plugin en wp-content/mu-plugins/.
Cabeceras esenciales y valores recomendados
Lista de cabeceras que conviene establecer desde PHP junto con explicaciones:
- Strict-Transport-Security (HSTS). Ejemplo recomendado: max-age=63072000 includeSubDomains preload
- Content-Security-Policy (CSP). Define orígenes permitidos por tipo de recurso (script-src, style-src, img-src, connect-src, font-src, frame-ancestors…).
- Referrer-Policy. Controla el Referer enviado: no-referrer-when-downgrade o strict-origin-when-cross-origin son opciones seguras.
- X-Content-Type-Options: nosniff. Evita que navegadores detecten tipo MIME incorrecto.
- X-Frame-Options o frame-ancestors (CSP). Previene clickjacking. SAMEORIGIN o usar frame-ancestors none.
- Permissions-Policy (antes Feature-Policy). Restringe APIs de navegador (geolocation, camera, microphone…).
- Report-To / report-uri. Para recibir informes de violaciones CSP. Report-To es más moderno, pero report-uri sigue siendo muy usado.
Política CSP: enfoque práctico
Dos enfoques comunes:
- Hashes (sha256-…): apropiado cuando hay pocos inline scripts estáticos conocidos.
- Nonces (nonce-…): recomendable para aplicaciones dinámicas (WordPress) porque puedes generar un nonce por petición y añadirlo a scripts/styles encolados.
Recomendación práctica: usar nonces para scripts y estilos que imprime WordPress y evitar inline scripts en temas/plugins externalizar scripts a archivos y encolarlos. Para librerías externas (CDN, analíticas), explícitalas en script-src con origenes concretos o cargar mediante proxy.
Implementación completa desde PHP (mu-plugin)
Ejemplo de mu-plugin que:
- Genera un nonce por petición.
- Envía HSTS (si es SSL).
- Envía una CSP que permite recursos propios y los scripts/estilos con nonce.
- Añade el nonce a los tags de scripts y estilos encolados por WordPress.
- Registra un endpoint REST para recibir reportes CSP.
lt?php / Plugin Name: WP Security Headers (mu) Description: Añade HSTS, CSP con nonces y cabeceras complementarias. Place in: wp-content/mu-plugins/wp-security-headers.php / // Genera y expone nonce por petición add_action(muplugins_loaded, function() { if (! function_exists(random_bytes)) { return } // Generamos un nonce base64 URL-safe nonce = rtrim(strtr(base64_encode(random_bytes(18)), /, -_), =) // Almacenar en variable global accesible en tiempo de render GLOBALS[wp_csp_nonce] = nonce }) // Añade cabeceras (use send_headers para que WP permita modificarlas antes de salida) add_action(send_headers, function() { // Solo enviar HSTS y CSP en HTTPS if (!is_ssl()) { // Podemos enviar otras cabeceras no relacionadas con HSTS si se desea return } // HSTS: 2 años recomendado como ejemplo ajustar según política header(Strict-Transport-Security: max-age=63072000 includeSubDomains preload) // Cabeceras complementarias header(X-Content-Type-Options: nosniff) header(Referrer-Policy: strict-origin-when-cross-origin) header(X-Frame-Options: SAMEORIGIN) // fallback header(Permissions-Policy: geolocation=(), camera=(), microphone=()) // Construir CSP usando nonce si existe nonce = isset(GLOBALS[wp_csp_nonce]) ? GLOBALS[wp_csp_nonce] : // Ejemplo de política. En producción revisa y añade orígenes necesarios (CDNs, APIs). csp_parts = [] csp_parts[] = default-src self csp_parts[] = script-src self nonce-{nonce} strict-dynamic csp_parts[] = style-src self nonce-{nonce} csp_parts[] = img-src self data: csp_parts[] = font-src self data: csp_parts[] = connect-src self https://api.example.com csp_parts[] = frame-ancestors none csp_parts[] = base-uri self csp_parts[] = form-action self // Modo de reporte (cambiar a report-to si se usa Report-To) csp_parts[] = report-uri /wp-json/security/v1/csp-report csp_header = implode( , csp_parts) header(Content-Security-Policy: {csp_header}) }, 100) // Añadimos el nonce a los scripts encolados por WP add_filter(script_loader_tag, function(tag, handle, src) { if (!empty(GLOBALS[wp_csp_nonce])) { // Inserta el atributo nonce en la etiqueta