Prévenez les failles des scripts DOM basés sur des scripts intersites grâce aux Trusted Types

Krzysztof Kotowicz
Krzysztof Kotowicz

Navigateurs pris en charge

  • Chrome: 83.
  • Edge: 83.
  • Firefox: non compatible.
  • Safari: non compatible.

Source

Le script intersites basé sur le DOM (DOM XSS) se produit lorsque les données d'une source contrôlée par l'utilisateur (comme un nom d'utilisateur ou une URL de redirection extraite du fragment d'URL) atteignent un puits, qui est une fonction comme eval() ou un setter de propriété comme .innerHTML pouvant exécuter du code JavaScript arbitraire.

Les DOM XSS sont l'une des failles de sécurité Web les plus courantes. Il est fréquent que les équipes de développement les introduisent accidentellement dans leurs applications. Les types approuvés vous fournissent les outils nécessaires pour écrire, examiner la sécurité et protéger les applications contre les failles DOM XSS en sécurisant par défaut les fonctions d'API Web dangereuses. Les types approuvés sont disponibles en tant que polyfill pour les navigateurs qui ne les prennent pas encore en charge.

Contexte

Depuis de nombreuses années, les attaques par script intersites basées sur le DOM constituent l'une des failles de sécurité Web les plus courantes et les plus dangereuses.

Il existe deux types de script intersites. Certaines failles XSS sont causées par du code côté serveur qui crée de manière non sécurisée le code HTML constituant le site Web. D'autres ont une cause racine sur le client, où le code JavaScript appelle des fonctions dangereuses avec du contenu contrôlé par l'utilisateur.

Pour éviter les attaques XSS côté serveur, ne générez pas de code HTML en concatenant des chaînes. Utilisez plutôt des bibliothèques de modèles d'échappement automatique contextuel sécurisé, ainsi qu'une stratégie de sécurité du contenu basée sur un nonce pour atténuer davantage les bugs.

Les navigateurs peuvent désormais également aider à prévenir les attaques XSS basées sur le DOM côté client à l'aide des Trusted Types.

Présentation de l'API

Les types approuvés fonctionnent en verrouillant les fonctions de sink à risque suivantes. Vous en connaissez peut-��tre déjà certaines, car les fournisseurs de navigateurs et les frameworks Web vous déconseillent déjà d'utiliser ces fonctionnalités pour des raisons de sécurité.

Les types approuvés vous obligent à traiter les données avant de les transmettre à ces fonctions de destination. L'utilisation d'une seule chaîne échoue, car le navigateur ne sait pas si les données sont fiables:

À éviter
anElement.innerHTML  = location.href;
Lorsque les Trusted Types sont activés, le navigateur génère une exception TypeError et empêche l'utilisation d'un collecteur DOM XSS avec une chaîne.

Pour indiquer que les données ont été traitées de manière sécurisée, créez un objet spécial, un Trusted Type.

À faire
anElement.innerHTML = aTrustedHTML;
  
Lorsque les types approuvés sont activés, le navigateur accepte un objet TrustedHTML pour les embases qui s'attendent à des extraits HTML. Il existe également des objets TrustedScript et TrustedScriptURL pour d'autres collecteurs sensibles.

Les Trusted Types réduisent considérablement la surface d'attaque DOM XSS de votre application. Il simplifie les examens de sécurité et vous permet d'appliquer les vérifications de sécurité basées sur le type effectuées lors de la compilation, de l'analyse lint ou de l'association de votre code au moment de l'exécution, dans le navigateur.

Utiliser les types approuvés

Se préparer aux rapports sur les cas de non-respect de la Content Security Policy

Vous pouvez déployer un collecteur de rapports, tel que le reporting-api-processor ou le go-csp-collector Open Source, ou utiliser l'un des équivalents commerciaux. Vous pouvez également ajouter des cas de non-respect de la journalisation et du débogage personnalisés dans le navigateur à l'aide d'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();

ou en ajoutant un écouteur d'événements:

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

Ajouter un en-tête CSP réservé aux rapports

Ajoutez l'en-tête de réponse HTTP suivant aux documents que vous souhaitez migrer vers les types approuvés:

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

Toutes les infractions sont désormais signalées à //my-csp-endpoint.example, mais le site Web continue de fonctionner. La section suivante explique le fonctionnement de //my-csp-endpoint.example.

Identifier les cas de non-respect des Trusted Types

Désormais, chaque fois que les types approuvés détectent une infraction, le navigateur envoie un rapport à un report-uri configuré. Par exemple, lorsque votre application transmet une chaîne à innerHTML, le navigateur envoie le rapport suivant:

{
"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"
}
}

Cela signifie que dans https://my.url.example/script.js à la ligne 39, innerHTML a été appelé avec la chaîne commençant par <img src=x. Ces informations devraient vous aider à identifier les parties de code susceptibles d'introduire une attaque XSS DOM et qui doivent être modifiées.

Corriger les cas de non-respect

Il existe plusieurs options pour résoudre un cas de non-respect des types approuvés. Vous pouvez supprimer le code qui pose problème, utiliser une bibliothèque, créer une règle Trusted Type ou, en dernier recours, créer une règle par défaut.

Réécrire le code incriminé

Il est possible que le code non conforme ne soit plus nécessaire ou qu'il puisse être réécrit sans les fonctions à l'origine des non-conformités:

À faire
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
À éviter
el.innerHTML = '<img src=xyz.jpg>';

Utiliser une bibliothèque

Certaines bibliothèques génèrent déjà des types approuvés que vous pouvez transmettre aux fonctions de collecteur. Par exemple, vous pouvez utiliser DOMPurify pour nettoyer un extrait HTML et supprimer les charges utiles XSS.

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

DOMPurify est compatible avec Trusted Types et renvoie du code HTML nettoyé encapsulé dans un objet TrustedHTML afin que le navigateur ne génère pas d'infraction.

Créer une règle de type approuvé

Il est parfois impossible de supprimer le code à l'origine de l'infraction, et aucune bibliothèque n'est disponible pour nettoyer la valeur et créer un type approuvé. Dans ce cas, vous pouvez créer vous-même un objet de type approuvé.

Commencez par créer une règle. Les règles sont des usines de types approuvés qui appliquent certaines règles de sécurité à leur entrée:

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

Ce code crée une règle appelée myEscapePolicy, qui peut produire des objets TrustedHTML à l'aide de sa fonction createHTML(). Les règles définies écrivent en HTML les caractères < pour éviter la création d'éléments HTML.

Utilisez la règle comme suit:

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)>'

Utiliser une stratégie par défaut

Il est parfois impossible de modifier le code non conforme, par exemple si vous chargez une bibliothèque tierce à partir d'un CDN. Dans ce cas, utilisez une règle par défaut:

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

La règle nommée default est utilisée chaque fois qu'une chaîne est utilisée dans un collecteur qui n'accepte que le type Trusted.

Passer à l'application de la stratégie de sécurité du contenu

Lorsque votre application ne génère plus de cas de non-respect, vous pouvez commencer à appliquer les types approuvés:

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

Désormais, quelle que soit la complexité de votre application Web, la seule chose qui peut introduire une faille DOM XSS est le code d'une de vos règles. Vous pouvez encore renforcer la sécurité en limitant la création de règles.

Documentation complémentaire