Si has llegado aquí es porque quieres poner una Content Security Policy (CSP) y te da miedo, con razón, dejar la web en blanco al hacerlo. Esa cabecera es la que de verdad limita el daño cuando alguien consigue colar un script en tus páginas, pero también es la que más webs deja rotas cuando se copia de cualquier sitio. La buena noticia es que existe un modo de rodaje que te deja ver qué se rompería antes de romper nada. En este artículo vas a montar una CSP en modo informe, leer lo que reclama tu propia web y activarla en bloqueo solo cuando estés seguro. Nada de teoría de más: pasos, un ejemplo realista para WordPress y los errores que vas a ver en la consola.
Qué ataque para de verdad una CSP
Una CSP le dice al navegador de tu visitante de qué sitios tiene permiso para cargar cosas: scripts, estilos, imágenes, tipografías, marcos. Todo lo que no esté en esa lista, el navegador lo bloquea.
El ataque que esto frena tiene nombre: inyección de scripts (XSS). Imagina que alguien consigue insertar en un comentario, en un campo de tu formulario o en un plugin vulnerable una línea que carga un script desde un servidor suyo. Sin CSP, el navegador de tus visitantes ejecuta ese script como si fuera tuyo: puede robar sesiones, redirigir a otra web o, en una tienda, capturar los datos de la tarjeta en el momento del pago. Con una CSP que solo permite scripts de tu propio dominio, ese script externo no llega a ejecutarse. El agujero sigue ahí, pero el atacante no puede aprovecharlo para cargar su código.
Conviene tener claro el alcance desde el principio: la CSP protege al visitante dentro del navegador. No cierra la vulnerabilidad, no impide que entren en tu servidor y no limpia una web ya infectada. Es la última capa, la que contiene el daño. Encaja dentro del conjunto de cabeceras de seguridad HTTP que tu servidor manda al navegador, y es la más potente de todas.
Anatomía de una política: las directivas que vas a usar y ya
Una CSP es una sola línea larga formada por directivas separadas por punto y coma. Cada directiva controla un tipo de recurso. No necesitas conocerlas todas; con estas te apañas para el noventa por ciento de las webs:
default-src: la regla por defecto. Lo que no tenga directiva propia hereda de aquí. Casi siempre'self', que significa «solo mi propio dominio».script-src: de dónde puede cargar JavaScript. La directiva crítica, porque el script es lo que ejecuta código.style-src: de dónde pueden venir las hojas de estilo.img-src: de dónde pueden venir las imágenes.font-src: de dónde pueden venir las tipografías.connect-src: a dónde puede hacer peticiones el JavaScript (analítica, API, formularios por AJAX).frame-src: qué se puede incrustar en un marco (vídeos de YouTube, mapas, pasarelas).frame-ancestors: quién puede meter tu web dentro de un marco. Con'none'bloqueas el clickjacking sin necesitar la vieja cabeceraX-Frame-Options.
Los valores que más vas a repetir son 'self' (tu dominio), un dominio concreto como https://www.googletagmanager.com, y 'none' (nada). Evita como la peste dos comodines: * (cualquier origen) y 'unsafe-inline' en script-src, porque este último abre justo la puerta que la CSP venía a cerrar.
Por qué casi todas las webs se rompen al principio: inline y terceros
Cuando activas una CSP estricta de golpe, la web «se rompe» por dos motivos, y los dos son normales.
El primero es el código en línea (inline). WordPress, tus plugins y tu tema meten trozos de JavaScript y de CSS directamente dentro del HTML, no en ficheros aparte. Una CSP con script-src 'self' bloquea todo ese código en línea, porque no viene de un fichero de tu dominio: viene incrustado en la página. Resultado: se caen menús, deslizadores, ventanas emergentes y la mitad del panel de administración.
El segundo son los terceros. Tu web carga más cosas de fuera de las que crees: la tipografía de Google, el vídeo incrustado, el mapa, el chat de soporte, el píxel de publicidad, la pasarela de pago, la analítica. Cada uno vive en un dominio distinto y cada uno tiene que estar declarado. Si no los listas, el navegador los bloquea en silencio: la web parece cargada, pero el botón de pagar no responde y nadie te avisa.
Por eso no se copia la CSP de otra web: la de otro describe sus terceros, no los tuyos. Y por eso existe el modo informe, que es exactamente lo que vamos a usar para descubrir los tuyos sin romper nada.
Modo aprendizaje: Content-Security-Policy-Report-Only
La clave de todo esto es una cabecera hermana: Content-Security-Policy-Report-Only. Hace lo mismo que la CSP normal pero no bloquea nada: solo anota en la consola del navegador lo que habría bloqueado. Es un simulacro. Puedes tenerla puesta en una tienda que está facturando sin riesgo, porque no toca la experiencia del visitante.
El plan es este:
- Pones una política de partida en modo Report-Only.
- Navegas por toda tu web durante unos días como lo haría un cliente: portada, ficha de producto, carrito, pago, formulario de contacto, blog. Y también entras al panel de administración.
- Vas apuntando en la consola qué orígenes reclama.
- Los añades a la política.
- Cuando la consola deje de quejarse en el uso normal, cambias el nombre de la cabecera a
Content-Security-Policy(sin el-Report-Only) y ya está en bloqueo real.
Dónde se pone la cabecera depende de tu servidor. En un hosting compartido con Apache va en el .htaccess de la raíz (habitualmente /public_html/.htaccess), dentro de un bloque <IfModule mod_headers.c>:
<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; frame-ancestors 'none'"
</IfModule>
En un VPS con nginx va en el bloque server de tu sitio, con add_header y el sufijo always. En los dos casos, descarga una copia del fichero antes de tocarlo y, tras guardar, comprueba que la web sigue en pie. Con nginx, valida con nginx -t antes de recargar.
Una CSP realista para WordPress con analítica, fuentes y píxel
Este es un punto de partida para una web WordPress típica que usa Google Fonts, Google Analytics y un píxel. No la copies a ciegas en modo bloqueo: úsala en Report-Only y ajústala a lo que reclame tu consola, porque tus plugins cargarán cosas que no están aquí.
default-src 'self';
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://www.google-analytics.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://www.google-analytics.com;
frame-src https://www.youtube.com;
frame-ancestors 'none';
base-uri 'self'
Fíjate en dos concesiones que casi no vas a poder evitar en WordPress. La primera: 'unsafe-inline' en style-src. Muchos temas meten estilos en línea y quitarlo del todo obliga a un trabajo grande; en estilos el riesgo es mucho menor que en scripts, así que se tolera. La segunda: data: en img-src, porque WordPress y varios plugins incrustan iconos pequeños directamente como datos.
Lo que no debes tolerar es 'unsafe-inline' en script-src. Si tu web solo funciona con eso, la CSP deja de protegerte de lo que importaba. La salida correcta pasa por que el plugin o el tema usen ficheros de script de verdad, y si no hay manera, es una señal de que ese componente está mal hecho. Esto entra dentro de dejar bien montada la base del sitio, que es de lo que va la guía de configuración segura del hosting, HTTPS, correo y DNS.
Cómo leer los errores de la consola del navegador
Abre tu web en Chrome, pulsa F12 para abrir las herramientas de desarrollador y ve a la pestaña Console (Consola). Con la CSP en modo informe verás avisos como este:
[Report Only] Refused to load the script 'https://ejemplo-tercero.com/widget.js'
because it violates the following Content Security Policy directive: "script-src 'self' ...".
Se lee sin saber inglés técnico si te fijas en dos datos: qué recurso se ha rechazado (la URL, aquí https://ejemplo-tercero.com/widget.js) y qué directiva lo ha rechazado (aquí script-src). La solución es añadir el origen de ese recurso a esa directiva. En el ejemplo, añadirías https://ejemplo-tercero.com a script-src.
Un consejo para no volverte loco: no añadas orígenes de uno en uno recargando mil veces. Navega por toda la web con la consola abierta, deja que se acumulen los avisos, apúntalos todos juntos y haz una sola tanda de cambios. Y usa el sitio como lo usaría un cliente real, incluido el proceso de pago completo, porque la pasarela suele reclamar varios dominios que solo aparecen al llegar al último paso.
Cuándo una CSP no compensa el trabajo
Toca ser honesto. Una CSP bien hecha es de lo mejor que puedes poner, pero no siempre sale a cuenta, y montarla mal es peor que no tenerla.
Si tu web es una landing sencilla, sin formularios de pago ni datos de clientes, y ya tienes el resto de cabeceras y el mantenimiento al día, el esfuerzo de afinar una CSP quizá no te devuelva mucho. En una web así, empieza por lo barato: X-Content-Type-Options, Referrer-Policy y frame-ancestors 'none' te dan la mayor parte del valor en diez minutos.
Si en cambio tienes una tienda, un formulario que trata datos personales o has tenido ya un susto con inyección de código, la CSP sí vale cada minuto que le dediques, porque es la capa que evita que un script inyectado robe a tus clientes. Eso sí, móntala en modo informe y con calma. Nadie sensato activa una CSP en bloqueo un viernes por la tarde en una web que está vendiendo.
Y una consideración de dinero: esto no requiere contratar a nadie si tienes acceso al .htaccess o a la configuración de nginx y una tarde tranquila. Si tu web la lleva una agencia, entra dentro de lo que deberían saber dejar montado. Contratar una consultoría para «poner una cabecera» es tirar el dinero; otra cosa es una web grande con decenas de integraciones, donde afinar la política sí es un trabajo de horas que se paga.
Cuando tengas tu CSP activa, no te fíes de que «parece que funciona» desde el navegador donde llevas media hora con la caché caliente. Pasa PathScan a tu dominio y comprueba gratis si tu CSP se está aplicando de verdad: mira tu web desde fuera como lo haría cualquier visitante, sin registro y sin tocar nada, y te dice qué cabeceras estás enviando en realidad y cómo se ve el resto de tu dominio desde el exterior.