Mitiga las secuencias de comandos entre sitios (XSS) con una política de seguridad de contenido (CSP) estricta

Cómo implementar un CSP basado en nonces de scripts o hashes como defensa en profundidad contra el scripting entre sitios.

Lukas Weichselbaum
Lukas Weichselbaum

¿Por qué deberías implementar una política de seguridad de contenido (CSP) estricta?

Cross-site scripting (XSS) o secuencias de comandos en sitios cruzados, definida como la capacidad de inyectar scripts maliciosos en una aplicación web, ha sido una de las mayores vulnerabilidades de seguridad web durante más de una década.

La Política de seguridad de contenido (CSP) es una capa adicional de seguridad que ayuda a mitigar XSS. La configuración de un CSP implica agregar el encabezado HTTP Content-Security-Policy a una página web y establecer valores para controlar qué recursos puede cargar el agente de usuario para esa página. Este artículo explica cómo usar un CSP basado en nonces o hashes para mitigar XSS en lugar de los CSP basados en listas de permisos de host de uso común que a menudo dejan la página expuesta a XSS, ya que se pueden omitir en la mayoría de las configuraciones.

Una política de seguridad de contenido basada en nonces o hashes a menudo se denomina CSP estricto . Cuando una aplicación usa un CSP estricto, los atacantes que encuentran fallas en la inyección de HTML generalmente no podrán usarlos para forzar al navegador a ejecutar scripts maliciosos en el contexto del documento vulnerable. Esto se debe a que el CSP estricto solo permite scripts hash o scripts con el valor de nonce correcto generado en el servidor, por lo que los atacantes no pueden ejecutar el script sin conocer el nonce correcto para una respuesta determinada.

Por qué se recomienda un CSP estricto en lugar de los CSP de lista de permitidos

Si tu sitio ya tiene un CSP con este aspecto: script-src www.googleapis.com, ¡es posible que no sea eficaz contra las secuencias de comandos entre sitios (cross-site scripting)! Este tipo de CSP se denomina CSP de lista de permitidos y tiene un par de desventajas:

Esto hace que los CSP de listas de permisos sean generalmente ineficaces para evitar que los atacantes exploten XSS. Es por eso que se recomienda utilizar un CSP estricto basado en nonces o hashes criptográficos, lo que evita las trampas descritas anteriormente.

Allowlist CSP

: no protege eficazmente su sitio. ❌ - Debe ser altamente personalizado. 😓

Strict CSP

  • Protege eficazmente su sitio. ✅
  • Siempre tiene la misma estructura. 😌

¿Qué es una política de seguridad de contenido estricta?

Una política de seguridad de contenido estricta tiene la siguiente estructura y se habilita configurando uno de los siguientes encabezados de respuesta HTTP:

  • CSP estricto basado en nonce
Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

  • CSP estricto basado en hash
Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Las siguientes propiedades hacen que un CSP como el anterior sea "estricto" y, por lo tanto, seguro:

  • Utiliza nonces 'nonce-{RANDOM}' o hashes 'sha256-{HASHED_INLINE_SCRIPT}' para indicar qué <script> son confiables para el desarrollador del sitio y deben poder ejecutarse en el navegador del usuario.

  • Establece 'strict-dynamic' para reducir el esfuerzo de implementar un CSP basado en hash o nonce al permitir automáticamente la ejecución de scripts creados por un script que ya es de confianza. Esto también desbloquea el uso de la mayoría de las bibliotecas y widgets de JavaScript de terceros.

  • No se basa en listas de URL permitidas y, por lo tanto, no sufre omisiones de CSP comunes.

  • Bloquea scripts en línea que no son de confianza, como controladores de eventos en línea o javascript: URIs.

  • Restringe object-src para deshabilitar complementos peligrosos como Flash.

  • Restringe la base-uri para bloquear la inyección de etiquetas <base> Esto evita que los atacantes cambien la ubicación de los scripts cargados desde URL relativas.

Adopción de un CSP estricto

Para adoptar un CSP estricto, debes:

  1. Decidir si tu aplicación debe establecer un CSP basado en hash o nonce.
  2. Copia el CSP de la sección ¿Qué es una política de seguridad de contenido estricta? Y configúralo como un encabezado de respuesta en tu aplicación.
  3. Refactorizar las plantillas HTML y el código del lado del cliente para eliminar patrones que sean incompatibles con CSP.
  4. Agregar alternativas para admitir Safari y navegadores más antiguos.
  5. Implementar tu CSP.

Puedes utilizar la auditoría de prácticas recomendadas Lighthouse (v7.3.0 y superior con flag --preset=experimental ) a lo largo de este proceso para comprobar si tu sitio tiene un CSP y si es lo suficientemente estricto como para ser eficaz contra XSS.

Lighthouse report warning that no CSP is found in enforcement mode.

Paso 1: Decide si necesitas un CSP basado en hash o en nonce

Hay dos tipos de CSP estrictos, basados en hash y basados en nonce. Así es como funcionan:

  • CSP basado en Nonce: generas un número aleatorio en tiempo de ejecución, lo incluyes en tu CSP y lo asocias con cada etiqueta de secuencia de comandos en tu página. Un atacante no puede incluir y ejecutar un script malicioso en su página, porque necesitaría adivinar el número aleatorio correcto para ese script. Esto solo funciona si el número no se puede adivinar y se genera nuevamente en tiempo de ejecución para cada respuesta.
  • CSP basado en hash: el hash de cada etiqueta de secuencia de comandos en línea se agrega al CSP. Ten en cuenta que cada secuencia de comandos tiene un hash diferente. Un atacante no puede incluir y ejecutar un script malicioso en tu página, porque el hash de ese script debería estar presente en tu CSP.

Criterios para elegir un enfoque de CSP estricto:

Criterios para elegir un enfoque de CSP estricto
CSP basado en Nonce Para páginas HTML renderizadas en el servidor donde puedes crear un nuevo token aleatorio (nonce) para cada respuesta.
CSP basado en hash Para páginas HTML servidas estáticamente o aquellas que necesitan ser almacenadas en caché. Por ejemplo, aplicaciones web de una sola página creadas con marcos como Angular, React u otros, que se sirven estáticamente sin renderizado del lado del servidor.

Paso 2: Establece un CSP estricto y prepara tus scripts

Al configurar un CSP, tienes algunas opciones:

  • Modo de solo informe ( Content-Security-Policy-Report-Only ) o modo de aplicación (Content-Security-Policy). En el modo de solo informe, el CSP no bloqueará los recursos todavía, nada se romperá, pero podrá ver errores y recibir informes de lo que se habría bloqueado. Localmente, cuando estás en el proceso de configurar un CSP, esto realmente no importa, porque ambos modos te mostrarán los errores en la consola del navegador. En todo caso, el modo de aplicación te facilitará aún más ver los recursos bloqueados y modificar tu CSP, ya que tu página se verá rota. El modo de solo informe se vuelve más útil más adelante en el proceso (consulte el Paso 5 ).
  • Encabezado o etiqueta HTML <meta>. Para el desarrollo local, una <meta> puede ser más conveniente para ajustar tu CSP y ver rápidamente cómo afecta tu sitio. Sin embargo:
    • Más adelante, al implementar tu CSP en producción, se recomienda configurarlo como un encabezado HTTP.
    • Si deseas configurar tu CSP en modo de solo informe, deberás configurarlo como un encabezado; las metaetiquetas de CSP no admiten el modo de solo informe.

Opción A: CSP basado en Nonce

Establece el siguiente encabezado de respuesta HTTP Content-Security-Policy

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Generar un nonce para CSP

Un nonce es un número aleatorio que se usa solo una vez por carga de página. Un CSP basado en nonce solo puede mitigar XSS si un atacante no puede adivinar el valor de nonce. Un nonce para CSP debe ser:

  • Un valor aleatorio criptográficamente fuerte (idealmente 128+ bits de longitud)
  • Recién generado para cada respuesta
  • Codificado en Base64

A continuación, se muestran algunos ejemplos sobre cómo agregar un nonce CSP en marcos del lado del servidor:

const app = express();
app.get('/', function(request, response) {
    // Generate a new random nonce value for every response.
    const nonce = crypto.randomBytes(16).toString("base64");
    // Set the strict nonce-based CSP response header
    const csp = script-src 'nonce-${nonce}' 'strict-dynamic' https:; object-src 'none'; base-uri 'none';;
    response.set("Content-Security-Policy", csp);
    // Every <script> tag in your application should set the nonce attribute to this value.
    response.render(template, { nonce: nonce });
  });
}

Agregar un nonce a los elementos <script>

Con un CSP basado en nonce, cada <script> debe tener un nonce que coincida con el valor nonce aleatorio especificado en el encabezado del CSP (todos los scripts pueden tener el mismo nonce). El primer paso es agregar estos atributos a todos los scripts:

Blocked by CSP
<script src="/https/web.developers.google.cn/path/to/script.js"></script>
<script>foo()</script>

CSP bloqueará estos scripts porque no tienen atributos nonce

Opción B: Encabezado de respuesta de CSP basado en hash

Establece el siguiente encabezado de respuesta HTTP Content-Security-Policy

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Para varios scripts en línea, la sintaxis es la siguiente: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}' .

Cargar scripts de origen dinámicamente

Todas las secuencias de comandos de origen externo deben cargarse dinámicamente a través de una secuencia de comandos en línea, ya que los hashes CSP sólo son compatibles con los navegadores para las secuencias de comandos en línea (los hashes para las secuencias de comandos de origen no son compatibles con los navegadores).

Blocked by CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>

CSP bloqueará estas secuencias de comandos, ya que solo se pueden aplicar hash a las secuencias de comandos en línea.

Allowed by CSP
<script>
var scripts = [ &#39;https://example.org/foo.js&#39;, &#39;https://example.org/bar.js&#39;];
scripts.forEach(function(scriptUrl) {
  var s = document.createElement(&#39;script&#39;);
  s.src = scriptUrl;
  s.async = false; // to preserve execution order
  document.head.appendChild(s);
});
</script>

Para permitir la ejecución de esta secuencia de comandos, el hash de la secuencia de comandos en línea debe calcularse y agregarse al encabezado de respuesta de CSP, reemplazando el marcador de posición {HASHED_INLINE_SCRIPT}. Para reducir la cantidad de hashes, opcionalmente puedes combinar todos los scripts en línea en un solo script. Para ver esto en la práctica, consulta el ejemplo y examine el código.

Opción B: Encabezado de respuesta de CSP basado en hash

En el fragmento de código anterior, se añade s.async = false para garantizar que foo se ejecute antes que bar (incluso si bar se carga primero). En este fragmento, s.async = false no bloquea el analizador mientras se cargan los scripts; eso es porque los scripts se agregan dinámicamente. El analizador solo se detendrá mientras se ejecutan los scripts, tal como se comportaría con los scripts async. Sin embargo, con este fragmento, ten en cuenta:

  • Uno o ambos scripts pueden ejecutarse antes de que el documento haya terminado de descargarse. Si deseas que el documento esté listo para cuando se ejecuten los scripts, debes esperar al evento DOMContentLoaded antes de agregar los scripts. Si esto causa un problema de rendimiento (porque los scripts no comienzan a descargarse lo suficientemente temprano), puedes usar etiquetas de precarga anteriormente en la página.
  • defer = true no hará nada. Si necesitas ese comportamiento, tendrás que ejecutar manualmente el script en el momento en que desees ejecutarlo.

Paso 3: Refactoriza las plantillas HTML y el código del lado del cliente para eliminar patrones incompatibles con CSP

Los controladores de eventos en línea (como onclick="…", onerror="…" ) y los URI de JavaScript ( <a href="javascript:…"> ) se pueden utilizar para ejecutar scripts. Esto significa que un atacante que encuentre un error XSS podría inyectar este tipo de HTML y ejecutar JavaScript malicioso. Un CSP basado en hash o nonce no permite el uso de dicho marcado. Si tu sitio utiliza alguno de los patrones descritos anteriormente, deberás refactorizarlos en alternativas más seguras.

Si habilitaste CSP en el paso anterior, podrás ver las infracciones de CSP en la consola cada vez que CSP bloquee un patrón incompatible.

Informes de infracción de CSP en la consola de desarrollador de Chrome.

En la mayoría de los casos, la solución es sencilla:

Para refactorizar los controladores de eventos en línea, vuelve a escribirlos para agregarlos desde un bloque de JavaScript

Blocked by CSP
<span onclick="doThings();">A thing.</span>

CSP bloqueará los controladores de eventos en línea.

Allowed by CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things')
          .addEventListener('click', doThings);
</script>

CSP permitirá controladores de eventos que se registren a través de JavaScript.

Para javascript: URIs, puedes usar un patrón similar

Blocked by CSP
<a href="javascript:linkClicked()">foo</a>

CSP bloqueará javascript: URIs.

Allowed by CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo')
          .addEventListener('click', linkClicked);
</script>

CSP permitirá controladores de eventos que se registren a través de JavaScript.

Uso de eval() en JavaScript

Si tu aplicación usa eval() para convertir las serializaciones de cadenas JSON en objetos JS, debes refactorizar dichas instancias a JSON.parse(), que también es más rápido.

Si no puede eliminar todos los usos de eval(), aún puedes establecer un CSP estricto basado en nonce, pero tendrás que usar 'unsafe-eval' que hará que tu política sea un poco menos segura.

Puedes encontrar estos y más ejemplos de refactorización de este tipo en este estricto CSP Codelab:

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi" loading="lazy" src="https://glitch.com/embed/#!/embed/strict-csp-codelab?attributionHidden=true&sidebarCollapsed=true&path=demo%2Fsolution_nonce_csp.html&highlights=14%2C20%2C28%2C39%2C40%2C41%2C42%2C43%2C44%2C45%2C54%2C55%2C56%2C57%2C58%2C59%2C60&previewSize=35" style="height: 100%; width: 100%; border: 0;" title="strict-csp-codelab on Glitch"

Paso 4: Agrega alternativas para admitir Safari y navegadores más antiguos

CSP es compatible con todos los navegadores principales, pero necesitará dos alternativas:

  • El uso de 'strict-dynamic' requiere agregar https: como respaldo para Safari, el único navegador importante sin soporte para 'strict-dynamic'. Al hacerlo:

    • Todos los navegadores que admiten 'strict-dynamic' ignorarán https: fallback, por lo que esto no reducirá la solidez de la política.
    • En Safari, los scripts de fuentes externas solo podrán cargarse si provienen de un origen HTTPS. Esto es menos seguro que un CSP estricto, es una alternativa, pero aún evitaría ciertas causas comunes de XSS como inyecciones de javascript: URIs porque 'unsafe-inline' no está presente o se ignora en presencia de un hash o un nonce.
  • Para garantizar la compatibilidad con versiones de navegador muy antiguas (más de 4 años), puedes agregar 'unsafe-inline' como respaldo. Todos los navegadores recientes ignorarán 'unsafe-inline' si hay un código de acceso o un hash de CSP.

Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Paso 5: Implementa tu CSP

Después de confirmar que CSP no está bloqueando scripts legítimos en tu entorno de desarrollo local, puedes continuar con la implementación de tu CSP en su (puesta en escena, luego) entorno de producción:

  1. (Opcional) Implementa tu CSP en modo de solo informe mediante el Content-Security-Policy-Report-Only. Puedes obtener más información sobre la API de informes. El modo de solo informe es útil para probar un cambio potencialmente importante como un nuevo CSP en producción, antes de aplicar las restricciones de CSP. En el modo de solo informe, tu CSP no afecta el comportamiento de tu aplicación (nada realmente se romperá). Pero el navegador seguirá generando errores de consola e informes de infracción cuando se encuentren patrones incompatibles con CSP (para que pueda ver lo que habría fallado para sus usuarios finales).
  2. Una vez que estés seguro de que tu CSP no provocará daños para tus usuarios finales, implementa tu CSP utilizando el encabezado de respuesta Content-Security-Policy Solo una vez que hayas completado este paso, CSP comenzará a proteger tu aplicación de XSS. Configurar tu CSP a través de un encabezado HTTP del lado del servidor es más seguro que configurarlo como una etiqueta <meta>; usa un encabezado si puedes.

Limitaciones

En términos generales, un CSP estricto proporciona una fuerte capa adicional de seguridad que ayuda a mitigar XSS. En la mayoría de los casos, CSP reduce significativamente la superficie de ataque (patrones peligrosos como javascript: URIs están completamente desactivados). Sin embargo, según el tipo de CSP que estés utilizando (nonces, hashes, con o sin 'strict-dynamic'), hay casos en los que CSP no protege:

  • Si ingresas un script, pero hay una inyección directamente en el cuerpo o en el src de ese elemento <script>.
  • Si hay inyecciones en las ubicaciones de scripts creados dinámicamente (document.createElement('script') ), incluso en cualquier función de biblioteca que cree script basados en el valor de sus argumentos. Esto incluye algunas API comunes como .html() jQuery, así como .get() y .post() en jQuery <3.0.
  • Si hay inyecciones de plantilla en aplicaciones antiguas de AngularJS. Un atacante que pueda inyectar una plantilla AngularJS puede usarla para ejecutar JavaScript arbitrario.
  • Si la política contiene 'unsafe-eval', inyecciones en eval(), setTimeout() y algunas otras API de uso poco frecuente.

Los desarrolladores y los ingenieros de seguridad deben prestar especial atención a estos patrones durante las revisiones de código y las auditorías de seguridad. Puedes encontrar más detalles sobre los casos descritos anteriormente en esta presentación de CSP.

Otras lecturas