La Content-Security-Policy (CSP) es una cabecera de respuesta HTTP que indica al navegador qué fuentes de contenido tiene derecho a cargar y ejecutar. Bien definida, convierte una brecha de inyección de scripts en una simple entrada bloqueada en la consola. Mal entendida, se reduce a una línea copiada que no detiene nada o que rompe medio sitio. Esta guía describe un despliegue progresivo, de las directivas al modo de prueba, para una base que protege sin paralizar la producción.
Qué bloquea una Content-Security-Policy
El cross-site scripting (XSS) sigue siendo una de las vulnerabilidades web más extendidas: un atacante logra inyectar JavaScript en una página y ese código se ejecuta con los permisos de la víctima. La CSP actúa como defensa en profundidad. Aunque una inyección supere la validación de entrada, el navegador se niega a ejecutar un script cuyo origen no esté autorizado de forma explícita por la política. La cabecera no sustituye al escapado de datos; añade una segunda barrera que limita con fuerza el impacto de un error de la aplicación.
Anatomía de la cabecera: las directivas que conviene conocer
Una política es una lista de directivas separadas por puntos y coma. Cada directiva nombra un tipo de recurso y su lista de fuentes autorizadas, expresadas como palabras clave ('self', 'none') o como dominios.
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; connect-src 'self' https://api.ejemplo.com; object-src 'none'| Directiva | Función |
|---|---|
default-src | Valor de reserva para las directivas no definidas |
script-src | Orígenes autorizados para el JavaScript |
style-src | Orígenes autorizados para las hojas de estilo |
img-src | Orígenes de las imágenes (a menudo 'self' data:) |
connect-src | Destinos de fetch, XHR, WebSocket, EventSource |
frame-ancestors | Quién puede incrustar la página en un iframe (anti-clickjacking) |
base-uri | Restringe la etiqueta <base>, a menudo 'self' |
object-src | Plugins heredados, conviene ponerlo a 'none' |
La directiva frame-ancestors merece una mención aparte: sustituye a la antigua cabecera X-Frame-Options y controla quién puede mostrar la página en un marco, lo que corta de raíz los ataques de clickjacking.
La trampa de ‘unsafe-inline’: nonces y huellas
La tentación más habitual es añadir 'unsafe-inline' a script-src para que los scripts incrustados en el HTML sigan funcionando. Esa palabra clave anula lo esencial de la protección: autoriza justamente el tipo de script que un atacante busca inyectar. Dos mecanismos permiten autorizar un script inline concreto sin abrir la puerta a todos los demás. El nonce es un valor aleatorio, regenerado en cada respuesta, colocado a la vez en la cabecera y en la etiqueta.
<!-- Cabecera: script-src 'nonce-r4Nd0mBase64' 'strict-dynamic' -->
<script nonce="r4Nd0mBase64">
// este bloque concreto está autorizado, ningún otro inline lo está
</script>La huella ('sha256-…') sigue la misma lógica para un script de contenido fijo. La palabra clave 'strict-dynamic' completa el dispositivo: la confianza otorgada a un script con nonce se propaga a los scripts que este carga, lo que evita mantener una larga lista de dominios.
Añadir ‘unsafe-inline’ a script-src equivale a autorizar precisamente el tipo de código que un ataque XSS busca inyectar.
Desplegar sin romper la producción: el modo Report-Only
Activar una política estricta de golpe en un sitio existente casi siempre provoca roturas: una etiqueta de seguimiento, una fuente externa o un widget de terceros acaban bloqueados. La cabecera Content-Security-Policy-Report-Only resuelve esto. Aplica la política en modo observación: el navegador no bloquea nada, pero informa de cada violación a un punto de recogida. La lista de recursos rechazados permite ajustar la política antes de imponerla de verdad.
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;Punto de atención: empiece siempre por Report-Only en un sitio en producción, déjelo correr varios días para cubrir todos los recorridos y luego cambie a la cabecera que bloquea. Compruebe también que el proxy o el CDN no reescriba la cabecera, so pena de aplicar una política distinta de la prevista.
Recoger y leer los informes de violación
El mecanismo histórico, report-uri, pide al navegador que envíe un documento JSON en cada violación, describiendo la página afectada, la directiva infringida y el recurso bloqueado. El enfoque moderno asocia la cabecera Reporting-Endpoints con la directiva report-to, mejor integrada con la API de reporting del navegador. Una ruta de servidor ligera basta para recoger estos envíos.
{
"csp-report": {
"document-uri": "https://ejemplo.com/cuenta",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://cdn.terceros.com/widget.js"
}
}La lectura regular de estos informes saca a la luz las fuentes de terceros que hay que autorizar de forma explícita: una fuente servida desde un CDN, un mapa o un vídeo incrustado, un script de medición de audiencia. Cada una se convierte en una línea asumida de la política, nunca en un permiso general concedido a toda una categoría.
Una base de partida razonable
Para un sitio clásico servido por Nginx, una política restrictiva pero funcional puede servir de base, para ampliarla directiva a directiva según los informes del modo observación.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests" always;La directiva upgrade-insecure-requests pide al navegador que convierta las peticiones HTTP residuales en HTTPS, una red útil durante una migración. Cada fuente añadida debe responder a una necesidad identificada, nunca a un bloqueo resuelto por comodidad con 'unsafe-inline'.
Lo que conviene recordar
La CSP es una capa de defensa que limita el impacto de una inyección cuando todo lo demás ha fallado. El método que aguanta se apoya en tres principios: desterrar 'unsafe-inline' en favor de los nonces o las huellas, desplegar primero en Report-Only para cartografiar los recursos legítimos y luego apretar directiva a directiva. Una política construida en ese orden protege de forma duradera sin convertir cada publicación en una caza de regresiones.
En los sitios que mantengo, siempre dejo la CSP en Report-Only durante al menos una semana antes de hacerla bloqueante, y centralizo los informes para releerlos con calma. Es tedioso, pero es la única forma que he encontrado de evitar el escenario clásico: una política estricta activada un viernes por la noche y el formulario de contacto sin responder el lunes. La CSP no es un interruptor, es un ajuste progresivo. — Simon Janvier
Para profundizar
Referencia de las directivas y su sintaxis: Content-Security-Policy en MDN Web Docs.
