La seguridad web no termina en validar entradas o usar consultas preparadas. Ya vimos en el blog cómo protegerte de SQL Injection, XSS y CSRF. Hoy cerramos esta serie de seguridad hablando de una capa que muchas veces se pasa por alto: las cabeceras de seguridad HTTP en PHP.
Estas cabeceras son instrucciones que tu servidor envía al navegador en cada respuesta, indicándole cómo comportarse: qué recursos puede cargar, si puede mostrarse dentro de un iframe ajeno, si debe forzar HTTPS, etc. Configurarlas bien no evita que existan vulnerabilidades, pero sí reduce mucho el daño que pueden causar si algo falla.
¿Qué son y por qué importan las cabeceras de seguridad HTTP?
Cuando abres cualquier web, el navegador no solo recibe el HTML: recibe también una serie de cabeceras HTTP que viajan junto a la respuesta. La mayoría de desarrolladores nunca las tocan, así que PHP y Apache usan sus valores por defecto, que no siempre son los más seguros.
Las cabeceras que de verdad importan
Content-Security-Policy (CSP)
Es la más potente y la más difícil de configurar bien. Le dice al navegador desde qué orígenes puede cargar scripts, estilos, imágenes, etc. Si un atacante consigue inyectar un <script> mediante XSS pero tu CSP no permite scripts externos, el navegador simplemente lo bloquea.
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline';");Lenguaje del código: JavaScript (javascript)Empieza siempre con Content-Security-Policy-Report-Only para ver qué bloquearía sin cortar nada todavía, y ve ajustando la lista de orígenes permitidos antes de aplicarla de verdad.
Strict-Transport-Security (HSTS)
Obliga al navegador a usar siempre HTTPS con tu dominio, incluso si alguien teclea http:// a mano o hace clic en un enlace antiguo. Esto evita ataques de intermediario que aprovechan esa primera conexión insegura.
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");Lenguaje del código: JavaScript (javascript)El valor 31536000 son los segundos de un año: mientras esta cabecera esté activa, el navegador recordará no usar HTTP nunca con tu dominio. Actívala solo cuando estés seguro de que todo tu sitio funciona correctamente por HTTPS.
X-Content-Type-Options
Evita que el navegador intente adivinar el tipo de un archivo distinto al que declaras. Sin esta cabecera, un archivo subido como imagen podría llegar a interpretarse como script en navegadores antiguos.
header("X-Content-Type-Options: nosniff");Lenguaje del código: JavaScript (javascript)X-Frame-Options
Controla si tu web puede mostrarse dentro de un <iframe> de otra página. Sin esta protección, un atacante podría montar un ataque de clickjacking: superponer tu web de forma invisible sobre botones falsos para que el usuario haga clic sin saber que está interactuando con tu sitio.
header("X-Frame-Options: SAMEORIGIN");Lenguaje del código: JavaScript (javascript)SAMEORIGIN permite que tu propia web se cargue en iframes de tu propio dominio, pero bloquea cualquier otro. Si no necesitas iframes en absoluto, usa DENY.
Referrer-Policy
Controla cuánta información sobre la página de origen se envía cuando un usuario hace clic en un enlace hacia otro sitio. Por defecto, los navegadores pueden filtrar la URL completa, incluyendo parámetros que no quieres compartir.
header("Referrer-Policy: strict-origin-when-cross-origin");Lenguaje del código: JavaScript (javascript)Permissions-Policy
Desactiva de forma explícita funcionalidades del navegador que tu web no necesita: cámara, micrófono, geolocalización, etc. Aunque nunca uses esas APIs, dejar la puerta abierta es innecesario si un script de terceros (o uno inyectado) intentara usarlas.
header("Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()");Lenguaje del código: JavaScript (javascript)Todo junto en PHP
Puedes añadir todas estas cabeceras en un único punto de entrada de tu aplicación, o en WordPress engancharte al hook send_headers, como hacemos en este mismo blog:
add_action('send_headers', function () {
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com;");
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: SAMEORIGIN");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()");
});Lenguaje del código: JavaScript (javascript)Alternativa: configurarlas en Apache (.htaccess)
Si prefieres no tocar PHP, puedes añadir las mismas cabeceras directamente en tu .htaccess (necesitas el módulo mod_headers activo en tu servidor):
<IfModule mod_headers.c>
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 Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>Lenguaje del código: JavaScript (javascript)Cómo comprobar que está todo bien configurado
No hace falta revisarlo a ojo: el OWASP Secure Headers Project documenta todas las cabeceras recomendadas y sus valores, y existen escáneres online que analizan tu sitio en segundos y te dicen exactamente qué te falta o qué tienes mal configurado.
Buenas prácticas
- Empieza CSP en modo
Report-Onlyantes de aplicarlo de verdad. - No actives HSTS hasta confirmar que todo tu sitio funciona en HTTPS sin excepciones.
- Usa
SAMEORIGINen X-Frame-Options salvo que necesites incrustar tu web en otro dominio de confianza. - Revisa las cabeceras cada vez que añadas un script de terceros nuevo (Analytics, anuncios, chats…): pueden requerir ajustar el CSP.
- No confíes solo en las cabeceras: son una capa más, no sustituyen validar entradas ni usar consultas preparadas.