Usando CSP para incrementar la seguridad de tu sitio web

Hacía ya algún tiempo venía manejando la idea de implementar CSP en mi blog de WordPress. CSP -o Content Security Policy, por sus siglas en inglés- es un estándar que define un conjunto de orígenes pre-aprobados desde los que un navegador puede cargar recursos cuando visita un sitio web. Ayuda a prevenir ataques de Cross Site Scripting (XSS), data injection, entre otros.

Cuando el navegador visita un sitio web tiene que cargar fuentes, hojas de estilo, imágenes, scripts, etc. Por ejemplo, este blog utiliza la fuente Open Sans de Google por lo que cada vez que alguien lo visite su navegador estará haciendo una llamada hacia el dominio fonts.googleapis.com. También utiliza las fotos de perfil de Gravatar, a través de secure.gravatar.com. Y así con el resto de los recursos.

Listado de dominios desde donde blog.pablofain.com carga recursos

Para habilitar CSP, el sitio web debe enviar un encabezado HTTP especial llamado Content-Security-Policy (también conocido como X-Content-Security-Policy). Como alternativa al encabezado HTTP se puede implementar CSP utilizando la etiqueta <meta>. Por ejemplo:

meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self';">

Podríamos decir que CSP es una suerte de lista blanca de orígenes y, por ende, una forma de prevenir ataques por inyección de código malicioso.

Cada plataforma web tiene su forma de manejar los encabezados HTTP. Para WordPress, por ejemplo, puedo recomendarles el plugin HTTP Headers. Este plugin permite enviar una serie de encabezados HTTP adicionales, incluyendo CSP, y es muy fácil de configurar.

Directivas de CSP en blog.pablofain.com

Directivas

CSP ofrece una gran cantidad de directivas configurables. La lista completa de directivas está publicada aquí, pero a continuación les dejo una lista de las más importantes.

default-src: Define la política de carga de recursos de cualquier tipo en caso que la directiva específica (por ejemplo, image-src) no esté definida.

script-src: Define desde dónde se pueden ejecutar script en el sitio web.

object-src: Define desde dónde se pueden cargar complementos.

style-src: Define desde dónde se pueden cargar las hojas de estilo (CSS).

img-src: Define desde dónde se cargarán las imágenes del sitio web.

media-src: Define desde dónde se cargarán archivos de audio y video.

frame-src: Define qué orígenes pueden ser mostrados dentro de un frame o iframe.

font-src: Define desde dónde se pueden cargar las fuentes.

report-uri: Especifica una URI a la que el navegador reportará violaciones a CSP (incluyendo Report-Only, del que hablaremos más adelante).

Vamos con algunos ejemplos para blog.pablofain.com.

Content-Security-Policy: default-src 'self' permite la carga de recursos (cualquiera de ellos) siempre que sean servidos desde blog.pablofain.com.
Content-Security-Policy: default-src pablofain.com permite la carga de recursos desde pablofain.com (y subdominios), sin importar el puerto ni el protocolo.
Content-Security-Policy: default-src https://blog.pablofain.com:443 permite la carga de recursos desde blog.pablofain.com, solo a través del protocolo https y utilizando el puerto por defecto (443).
 
Content-Security-Policy: default-src 'self'; img-src 'self' pixel.wp.com permite la carga de cualquier tipo de recurso (excepto imágenes) desde blog.pablofain.com. Imágens pueden ser cargadas desde blog.pablofain.com y desde pixel.wp.com.

Además de la etiqueta ‘self’, que permite la carga de recursos desde el mismo origen, también podemos utilizar:

'none' para bloquear la carga de ese tipo de recursos.
'self' coincide con el origen actual (excluye subdominios).
'unsafe-inline' permite el uso de JS y CSS directamente embebido en el código.
'unsafe-eval' permite el uso de mecanismos como el eval().

script-src

Necesito que nos detengamos por algunos minutos en la directiva script-src. Esta directiva cumple un papel fundamental en toda política de CSP, pues es la que va a bloquear la ejecución de scripts explícitamente no permitidos en nuestro sitio web.

Por defecto, los inline scripts tienen que estar deshabilitados. Permitir inline scripts mediante el uso de la etiqueta ‘unsafe-inline’ es lo mismo que no tener una directiva de CSP definida para origen de scripts.

Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self';

Sin embargo, hay casos en los que el uso de scripts directamente en el código fuente del sitio es imperativo. Para estos casos existen los hashes y nonces. Tomemos el ejemplo de este blog, que corre sobre WordPress y usa el plugin de Jetpack.

Jetpack por defecto inserta en el código fuente del sitio un script por cada funcionalidad que se habilita (hay un issue abierto en GitHub por este tema). Como podemos ver a continuación, el blog está intentando cargar un inline script, no permitido por la directiva script-src.

La directiva del blog incluye algunos orígenes específicos, como syndication.twitter.com, cdn.ampproject.org, entre otros, y también algunos hashes, pero no el que Jetpack estaba intentando cargar. En consecuencia, el script no se ejecutóy el error quedó registrado en el navegador.

Para permitir la ejecución del script podríamos agregar su hash (ya calculado por el navegador en el mensaje de error) a nuestra directiva script-src, que quedaría de la sigueinte manera:

Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-gMBz87JQ1eF8twzbMaO+TsfxqyLQXY+J0rrx4mTM3jA='; img-src 'self';

La parte mala es que estos hashes son únicos para cada script. Si el script cambia en al menos una letra, el hash también lo hará. Para evitar tener que recalcular el hash todo el tiempo, podemos emplear el uso de nonces.

Un único nonce puede ser reutilizado en múltiples scripts. Para que funcione, cada script hará referencia al nonce declarado en la directiva CSP.

<script src="https://blog.pablofain.com/wp-content/resources/analytics.js" nonce="MyyeBK5U3Z7VCdid6dnV7FEU3wWk21KrsFcs1xU2"></script>

Este método sirve para aquellas páginas que son generadas dinámicamente, y deben que tener la capacidad de generar nonces lo suficientemente aleatorios como para prevenir que un atacante pueda predecirlos. Como dije anteriormente, se puede utiliza el mismo nonce para cada script, pero debe ser regenerado e insertado tanto en los scripts como en los encabezados HTTP o etiqueta <meta>con cada petición.

Si deciden utilizar hashes y nonces, les recomiendo mantener la documentación de su código actualizada para que les resulte fácil encontrar qué hash o nonce pertenece a qué script.

Identificar

La implementación de CSP require de, por lo menos, cuatro etapas. Identificación, monitoreo, corrección, y monitoreo nuevamente.

La etapa de identificación implica reconocer nuestro sitio y sus recursos. Entender qué estamos enviando al navegador del usuario y desde dónde. Una manera simple es inspeccionando los elementos directamente desde el navegador, o utilizando Fiddler para capturar el tráfico.

En la imagen anterior podemos identificar doce dominios diferentes desde los que se obtienen los recursos de blog.pablofain.com. Analizando cada uno de ellos obtendremos la información necesaria para armar nuestras directivas de CSP.

Monitorear

Una vez que tengamos armadas las directivas, vamos a implementar la política en modo Report-only. Esto significa que en lugar de enviar el encabezado HTTP Content-Security-Policy, vamos a enviar Content-Security-Policy-Report-Only.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; img-src 'self'; 

Y acá es donde entra a la cancha Report URI. Report URI permite, de manera gratuita, monitorear el uso de CSP y las violaciones a nuestra política. En esta imagen vemos que se produjeron tres violaciones a la directiva script-src.

A continuación vemos el detalle de una de ellas. Allí podemos observar que la línea de código número 381 de la página especificada en document-uri es la que está violando la directiva por intentar utilizar un inline script.

{
    "csp-report": {
        "document-uri": "https://blog.pablofain.com/",
        "effective-directive": "script-src",
        "original-policy": "default-src 'self'; script-src 'self';
        "blocked-uri": "inline",
        "line-number": 381
    }
}

Security Headers, por su parte, analiza los encabezados HTTP de nuestro sitio y nos brinda una calificación, así como también algunas recomendaciones para incrementar la seguridad.

Por ejemplo, si permitimos inline scripts vamos a recibir la siguiente advertencia.

Una vez que estemos completamente seguros que nuestras directivas de CSP tienen todo lo que necesitamos, podemos pasar de Report-Only a Enforced mode cambiando el encabezado HTTP a Content-Security-Policy.

Corregir

Nadie dijo que implementar CSP iba a ser fácil. Yo mismo dejé mi blog completamente destruido por olvidar que traigo algunas hojas de estilo desde el dominio wp.com 🙂

Por eso es IMPORTANTÍSIMA la etapa de monitoreo. De la mano de Report URI podemos identificar posibles errores en la configuración de las directivas de CSP antes que nuestros usuarios lo hagan por nosotros.

Conclusión

Los ataques por inyección de código malicioso son cada vez más comunes. Sin importar si estamos ofreciendo a nuestro visitante un artículo periodístico, los resultados del último superclásico, la posibilidad de comprar un electrodoméstico, o de consultar el estado de su tarjeta de crédito; todos los sitios deberían utilizar CSP y garantizarles que el contenido que reciben es el que realmente deben recibir.

¡Happy coding!

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.