CONFIGURACIÓN DE ENCABEZADOS HTTP PARA MEJORAR LA SEGURIDAD EN PHP: CSP, X-FRAME-OPTIONS Y HSTS
Los encabezados HTTP de seguridad son una de las medidas más efectivas y de menor costo para endurecer una aplicación web. Cuando se configuran correctamente, mitigan ataques como XSS, clickjacking y downgrade de HTTPS.Content-Security-Policy (CSP)
CSP es el encabezado más potente y flexible. Define de forma declarativa de dónde el navegador puede cargar recursos, como scripts, estilos, imágenes, fuentes, frames, etc. Su objetivo principal es prevenir y mitigar ataques XSS y de inyección de contenido.Directivas más usadas
- "default-src": valor por defecto para el resto de directivas.
- "script-src": controla de dónde se pueden cargar y ejecutar scripts.
- "style-src": estilos.
- "img-src", "font-src", "connect-src", "frame-src" / "child-src", "object-src", etc.
- "frame-ancestors": controla quién puede embeber la página, reemplaza en gran medida a "X-Frame-Options".
- "base-uri", "form-action", "upgrade-insecure-requests".
Ejemplo práctico en PHP
// En un archivo de bootstrap o middleware, antes de cualquier salida
$csp = [
"default-src 'self'",
"script-src 'self' 'nonce-" . $nonce . "' https://cdn.example.com",
"style-src 'self' 'unsafe-inline'", // idealmente eliminar 'unsafe-inline'
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests"
];
header("Content-Security-Policy: " . implode('; ', $csp));
X-Frame-Options
Este encabezado protege contra clickjacking, ataques de interfaz de usuario. Indica si el navegador debe permitir que la página se muestre dentro de un "iframe>", "frame" u "object".Valores posibles
- "DENY": nunca permitir el embebido.
- "SAMEORIGIN": solo permitir desde el mismo origen.
- "ALLOW-FROM uri": obsoleto y poco soportado.
Implementación en PHP
header('X-Frame-Options: DENY');
// o
header('X-Frame-Options: SAMEORIGIN');
//Nota: La directiva CSP frame-ancestors es más flexible y tiene mejor soporte. Se recomienda usar ambas durante la transición
header('X-Frame-Options: DENY');
header("Content-Security-Policy: frame-ancestors 'none'");
Strict-Transport-Security (HSTS)2h>
HSTS fuerza al navegador a comunicarse siempre por HTTPS con el dominio durante un período determinado. Evita ataques de downgrade, que intentan forzar HTTP.
Parámetros principales
- "max-age": tiempo en segundos que el navegador debe recordar la política, recomendado: 1 año = 31536000.
- "includeSubDomains": aplica también a todos los subdominios.
- "preload": permite incluir el dominio en la lista de precarga de HSTS de los navegadores, requiere cumplir requisitos estrictos.
Ejemplo en PHP
// Solo enviar este encabezado cuando la conexión ya es HTTPS
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
Advertencia crítica: Una vez que se envía HSTS con un "max-age" alto, los navegadores recordarán la política. Si se comete un error, como certificado inválido, subdominios sin HTTPS, se puede bloquear el acceso al sitio. Es mejor probar primero con un "max-age" bajo, por ejemplo 300, y aumentarlo progresivamente.
Implementación recomendada en una aplicación PHP
La mejor práctica es centralizar el envío de encabezados de seguridad en un solo lugar, como unmiddleware, front controller o archivo de configuración inicial:
// security-headers.php
function sendSecurityHeaders(string $nonce = ''): void
{
// HSTS (solo en HTTPS)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
// X-Frame-Options + CSP frame-ancestors
header('X-Frame-Options: DENY');
// CSP
$csp = "default-src 'self'; "
. "script-src 'self' 'nonce-{$nonce}'; "
. "style-src 'self'; "
. "img-src 'self' data: https:; "
. "font-src 'self'; "
. "connect-src 'self'; "
. "frame-ancestors 'none'; "
. "base-uri 'self'; "
. "form-action 'self'; "
. "object-src 'none'; "
. "upgrade-insecure-requests";
header("Content-Security-Policy: {$csp}");
// Otros encabezados útiles (recomendados)
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
}
//En el inicio de la aplicación
$nonce = base64_encode(random_bytes(16));
sendSecurityHeaders($nonce);
// Solo enviar este encabezado cuando la conexión ya es HTTPS
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
// security-headers.php
function sendSecurityHeaders(string $nonce = ''): void
{
// HSTS (solo en HTTPS)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}
// X-Frame-Options + CSP frame-ancestors
header('X-Frame-Options: DENY');
// CSP
$csp = "default-src 'self'; "
. "script-src 'self' 'nonce-{$nonce}'; "
. "style-src 'self'; "
. "img-src 'self' data: https:; "
. "font-src 'self'; "
. "connect-src 'self'; "
. "frame-ancestors 'none'; "
. "base-uri 'self'; "
. "form-action 'self'; "
. "object-src 'none'; "
. "upgrade-insecure-requests";
header("Content-Security-Policy: {$csp}");
// Otros encabezados útiles (recomendados)
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
}
//En el inicio de la aplicación
$nonce = base64_encode(random_bytes(16));
sendSecurityHeaders($nonce);