Content Security Policy (CSP)

Analizamos qué es CSP, cómo funciona y de qué forma podemos implementarlo para mitigar ataques de tipo XSS y otros vectores en nuestras aplicaciones web.

content-security-policy

El término Content Security Policy (CSP) es un mecanismo que los navegadores modernos admiten y que nos permite reducir significativamente la exposición a ataques de nuestras aplicaciones web. En particular, CSP está diseñado para prevenir inyecciones de código malicioso, como los ataques XSS, estableciendo qué fuentes de contenido están permitidas en una página.

Al definir una política de seguridad, limitamos de forma explícita los orígenes desde los que se pueden cargar recursos como scripts, estilos, fuentes, imágenes, etc. Evitando así posibles recursos maliciosos que podrían cagarse en nuestras páginas.

Cómo funciona CSP

CSP se basa en enviar una cabecera HTTP (Content-Security-Policy) o en incluir una etiqueta <meta> en el <head> del documento HTML. Dentro de esta cabecera o etiqueta, definimos una serie de directivas que especifican los orígenes permitidos para cada tipo de contenido.

Un ejemplo básico de política podría ser:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net

Esta política indica que, por defecto, solo se permiten recursos cargados desde el mismo origen ('self') y que los scripts pueden provenir tanto del mismo origen como de una url externa como https://cdn.jsdelivr.net.

Directivas más comunes

Estas son las directivas permitidas más habituales:

  • default-src: fuente por defecto para todos los recursos no especificados en otras directivas.
  • script-src: orígenes permitidos para scripts JavaScript.
  • style-src: orígenes de hojas de estilo.
  • img-src: orígenes de imágenes.
  • font-src: orígenes de fuentes tipográficas.
  • connect-src: URLs con las que el frontend puede establecer conexiones (AJAX, WebSocket, etc.).
  • frame-src: orígenes de contenido embebido vía <iframe>.
  • object-src: orígenes de objetos como Flash o plugins (hoy en desuso).

Algunos valores clave

  • 'self': permite recursos desde el mismo dominio.
  • 'none': bloquea totalmente ese tipo de recurso.
  • 'unsafe-inline': permite código embebido inline (desaconsejado).
  • 'unsafe-eval': permite el uso de eval() y similares (también desaconsejado).
  • URLs absolutas o subdominios específicos: podemos autorizar recursos concretos.

Ejemplo práctico

Supongamos que tenemos una aplicación que solo debe cargar scripts desde nuestro dominio y desde un CDN externo. También cargamos fuentes desde Google Fonts e imágenes desde un subdominio de nuestro propio sitio. Podríamos aplicar esta política:

Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' https://media.example.com;

Para establecer esta cabecera en una aplicación Node.js con Express, podemos usar middleware:

app.use((req, res, next) => {
res.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' https://fonts.googleapis.com; " +
"font-src https://fonts.gstatic.com; " +
"img-src 'self' https://media.example.com"
);
next();
});

O si usamos Nginx:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' https://media.example.com";

Si queremos definir el CSP en el documento HTML:

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' https://media.example.com;
">
<title>Ejemplo con CSP</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<h1>Política CSP con etiqueta meta</h1>
<p>Este documento aplica CSP directamente desde el HTML.</p>
</body>
</html>

Si en este último ejemplo probamos de insertar una imagen de un host no permitido como <img src="https://plus.unsplash.com/premium_photo-1670148434900-5f0af77ba500">

Nos saltará el error

Refused to load the image 'https://plus.unsplash.com/premium_photo-1670148434900-5f0af77ba500' because it violates the following Content Security Policy directive: "img-src 'self' https://media.example.com".

Consejos al implementar CSP

Conviene aplicar CSP de forma progresiva. Podemos empezar usando el modo Content-Security-Policy-Report-Only, que nos permite recibir informes de violaciones sin bloquear contenido:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

Esto nos ayuda a identificar recursos que serían bloqueados y adaptar la política con menos riesgo.

También es útil generar informes automáticos. Hay servicios externos que recogen los errores de CSP y nos los presentan en dashboards, como Report URI o Sentry.

Además, conviene evitar 'unsafe-inline' y 'unsafe-eval', ya que permiten la ejecución de código JavaScript embebido directamente en el HTML, lo cual puede abrir la puerta a ataques de inyección. Si necesitamos usar scripts inline por motivos técnicos, existen alternativas más seguras.

Una opción es usar nonces. Un nonce es un valor aleatorio generado por el servidor en cada respuesta. Este valor se asigna como atributo nonce a los scripts inline y se indica también en la cabecera CSP. Solo los scripts que incluyan el nonce correcto se ejecutarán.

Ejemplo:

<script nonce="X7z9aK2b">console.log("Este script se ejecuta porque tiene el nonce correcto")</script>

Y en la cabecera CSP:

Content-Security-Policy: script-src 'self' 'nonce-X7z9aK2b'

Otra opción son los hashes, que consisten en calcular un hash (por ejemplo SHA-256) del contenido exacto del script. Este hash se incluye en la política y el navegador permite ejecutar ese script solo si coincide.

Ejemplo:

<script>console.log("Este script se permite gracias a su hash")</script>

Y en la política (calculado previamente):

Content-Security-Policy: script-src 'self' 'sha256-AbCdEf123...'

Conclusión

Content Security Policy es una herramienta útil para reducir el riesgo de ejecución de código malicioso en nuestras aplicaciones web. Aunque puede requerir cierto ajuste inicial, especialmente en aplicaciones existentes, su implementación progresiva nos permite identificar posibles puntos vulnerables y reforzar la seguridad sin interrumpir el funcionamiento de la aplicación.