Evita las vulnerabilidades de secuencias de comandos entre sitios basadas en DOM con Trusted Types

Krzysztof Kotowicz
Krzysztof Kotowicz

Navegadores compatibles

  • Chrome: 83.
  • Edge: 83.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

La secuencia de comandos entre sitios basada en DOM (DOM XSS) ocurre cuando los datos de una fuente controlada por el usuario (como un nombre de usuario o una URL de redireccionamiento tomada del fragmento de URL) llegan a un sumidero, que es una función como eval() o un set de propiedades como .innerHTML que puede ejecutar código JavaScript arbitrario.

La XSS del DOM es una de las vulnerabilidades de seguridad web más comunes, y es habitual que los equipos de desarrollo la introduzcan accidentalmente en sus apps. Trusted Types te brinda las herramientas para escribir, revisar la seguridad y mantener las aplicaciones libres de vulnerabilidades de XSS del DOM, ya que hace que las funciones peligrosas de la API web sean seguras de forma predeterminada. Trusted Types está disponible como polyfill para los navegadores que aún no lo admiten.

Segundo plano

Durante muchos años, el XSS de DOM fue una de las vulnerabilidades de seguridad web más frecuentes y peligrosas.

Existen dos tipos de secuencias de comandos entre sitios. Algunas vulnerabilidades de XSS son causadas por código del servidor que crea de forma insegura el código HTML que forma el sitio web. Otros tienen una causa raíz en el cliente, en la que el código JavaScript llama a funciones peligrosas con contenido controlado por el usuario.

Para evitar XSS del servidor, no generes HTML concatenando cadenas. En su lugar, usa bibliotecas de plantillas de escape automático contextual seguro, junto con una política de seguridad del contenido basada en nonce para mitigar errores adicionales.

Ahora, los navegadores también pueden ayudar a evitar XSS basados en el DOM del cliente con el uso de Trusted Types.

Introducción a la API

Los tipos de confianza funcionan bloqueando las siguientes funciones de sink peligrosas. Es posible que ya reconozcas algunas de ellas, ya que los proveedores de navegadores y los frameworks web ya te desaconsejan usar estas funciones por motivos de seguridad.

Los tipos de confianza requieren que proceses los datos antes de pasarlos a estas funciones de sink. El uso de solo una cadena falla porque el navegador no sabe si los datos son confiables:

Qué no debes hacer
anElement.innerHTML  = location.href;
Con Trusted Types habilitado, el navegador arroja un TypeError y evita el uso de un sink de XSS del DOM con una cadena.

Para indicar que los datos se procesaron de forma segura, crea un objeto especial: un tipo de confianza.

Qué debes hacer
anElement.innerHTML = aTrustedHTML;
  
Con los tipos de confianza habilitados, el navegador acepta un objeto TrustedHTML para los sumideros que esperan fragmentos HTML. También hay objetos TrustedScript y TrustedScriptURL para otros sumideros sensibles.

Trusted Types reduce significativamente la superficie de ataque de XSS del DOM de tu aplicación. Simplifica las revisiones de seguridad y te permite aplicar las verificaciones de seguridad basadas en tipos que se realizan cuando se compila, lintea o agrupa tu código en el tiempo de ejecución, en el navegador.

Cómo usar los tipos de confianza

Prepárate para los informes de incumplimientos de la Política de Seguridad del Contenido

Puedes implementar un recopilador de informes, como reporting-api-processor o go-csp-collector de código abierto, o bien usar uno de los equivalentes comerciales. También puedes agregar registros personalizados y violaciones de depuración en el navegador con un ReportingObserver:

const observer = new ReportingObserver((reports, observer) => {
    for (const report of reports) {
        if (report.type !== 'csp-violation' ||
            report.body.effectiveDirective !== 'require-trusted-types-for') {
            continue;
        }

        const violation = report.body;
        console.log('Trusted Types Violation:', violation);

        // ... (rest of your logging and reporting logic)
    }
}, { buffered: true });

observer.observe();

o agregando un objeto de escucha de eventos:

document.addEventListener('securitypolicyviolation',
    console.error.bind(console));

Agrega un encabezado CSP solo para informes

Agrega el siguiente encabezado de respuesta HTTP a los documentos que deseas migrar a Tipos de confianza:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

Ahora, todos los incumplimientos se informan a //my-csp-endpoint.example, pero el sitio web sigue funcionando. En la siguiente sección, se explica cómo funciona //my-csp-endpoint.example.

Identifica los incumplimientos de los tipos de confianza

A partir de ahora, cada vez que Trusted Types detecte una infracción, el navegador enviará un informe a un report-uri configurado. Por ejemplo, cuando tu aplicación pasa una cadena a innerHTML, el navegador envía el siguiente informe:

{
"csp-report": {
    "document-uri": "https://my.url.example",
    "violated-directive": "require-trusted-types-for",
    "disposition": "report",
    "blocked-uri": "trusted-types-sink",
    "line-number": 39,
    "column-number": 12,
    "source-file": "https://my.url.example/script.js",
    "status-code": 0,
    "script-sample": "Element innerHTML <img src=x"
}
}

Esto indica que, en https://my.url.example/script.js, en la línea 39, se llamó a innerHTML con la cadena que comienza con <img src=x. Esta información debería ayudarte a reducir qué partes del código podrían estar introduciendo XSS de DOM y que deben cambiarse.

Corrige los incumplimientos

Existen dos opciones para corregir un incumplimiento de tipo de confianza. Puedes quitar el código infractor, usar una biblioteca, crear una política de Trusted Types o, como último recurso, crear una política predeterminada.

Vuelve a escribir el código infractor

Es posible que el código que no cumple con los requisitos ya no sea necesario o que se pueda escribir nuevamente sin las funciones que causan los incumplimientos:

Qué debes hacer
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
Qué no debes hacer
el.innerHTML = '<img src=xyz.jpg>';

Usa una biblioteca

Algunas bibliotecas ya generan Trusted Types que puedes pasar a las funciones de sink. Por ejemplo, puedes usar DOMPurify para limpiar un fragmento HTML y quitar las cargas útiles de XSS.

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});

DOMPurify admite Trusted Types y muestra HTML limpio unido en un objeto TrustedHTML para que el navegador no genere una infracción.

Crea una política de tipo de confianza

A veces, no puedes quitar el código que causa el incumplimiento, y no hay ninguna biblioteca para limpiar el valor y crear un tipo de confianza por ti. En esos casos, puedes crear un objeto de tipo de confianza por tu cuenta.

Primero, crea una política. Las políticas son fábricas de Trusted Types que aplican ciertas reglas de seguridad en su entrada:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: string => string.replace(/\</g, '&lt;')
  });
}

Este código crea una política llamada myEscapePolicy que puede producir objetos TrustedHTML con su función createHTML(). Las reglas definidas escapan de HTML a los caracteres < para evitar la creación de nuevos elementos HTML.

Usa la política de la siguiente manera:

const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x onerror=alert(1)>'

Usa una política predeterminada

En ocasiones, no puedes cambiar el código infractor, por ejemplo, si cargas una biblioteca de terceros desde una CDN. En ese caso, usa una política predeterminada:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  trustedTypes.createPolicy('default', {
    createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
  });
}

La política llamada default se usa siempre que se usa una cadena en un sumidero que solo acepta el tipo de confianza.

Cómo cambiar a la aplicación forzosa de la Política de Seguridad del Contenido

Cuando tu aplicación ya no genere incumplimientos, puedes comenzar a aplicar los tipos de confianza:

Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

Ahora, no importa lo compleja que sea tu aplicación web, lo único que puede introducir una vulnerabilidad de XSS del DOM es el código de una de tus políticas, y puedes bloquearlo aún más si limitas la creación de políticas.

Lecturas adicionales