Nettoyer le code HTML non approuvé

Fonctionnalités Web : API Sanitizer

L'affichage sécurisé du code HTML généré par l'utilisateur est un défi de sécurité courant. L'API Sanitizer native fournit un moyen intégré et optimisé pour le navigateur de supprimer les contenus dangereux (comme les balises <script> ou les attributs on*) avant qu'ils n'atteignent le DOM.

La correction côté client protège la page qui affiche le contenu. Si le même contenu généré par l'utilisateur est également affiché ailleurs (autres clients, pages affichées par le serveur ou e-mails), nettoyez-le sur le serveur ainsi que pour la défense en profondeur, et assurez-vous que les règles de nettoyage du serveur et du client ne sont pas en conflit.

1. Insérer du code HTML de manière sécurisée avec setHTML()

La principale façon d'utiliser l'API Sanitizer est d'utiliser la méthode setHTML() sur n'importe quel Element. Cette méthode gère l'analyse, la correction et l'insertion en une seule étape, ce qui garantit que le contenu est corrigé spécifiquement pour le contexte dans lequel il sera inséré.

const untrustedHTML = `
  <p>Hello!</p>
  <script>alert('XSS')</script>
  <img src="x" onerror="alert('XSS')">
  <div onclick="doBadThing()">Click me</div>
`;

const container = document.getElementById('output');

// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);

2. Utiliser des configurations de nettoyage personnalisées

Vous pouvez personnaliser les règles de nettoyage en créant une instance Sanitizer. Vous pouvez ainsi définir exactement les éléments et les attributs autorisés pour votre cas d'utilisation spécifique.

Lorsque vous utilisez Element.setHTML() et Document.parseHTML(), les paramètres Sanitizer par défaut suppriment tous les éléments et attributs non sécurisés contre les attaques XSS. Vous ne pouvez pas créer un Sanitizer plus permissif qui autorise ces éléments, mais vous pouvez restreindre davantage ce qui est autorisé.

// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
  elements: ["p", "b", "i", "strong", "em"],
  attributes: ["class"],
  replaceWithChildrenElements: ['div']
};

const mySanitizer = new Sanitizer(config);

// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });

Les éléments qui ne sont pas dans elements sont supprimés ainsi que leur contenu. Répertoriez les éléments de wrapper dont vous souhaitez conserver le texte (comme div ci-dessus) dans replaceWithChildrenElements, ce qui supprime l'élément, mais conserve ses enfants.

Si vous avez besoin d'un Sanitizer plus permissif qui autorise certains éléments ou attributs non sécurisés contre les attaques XSS, vous pouvez utiliser un outil de nettoyage personnalisé avec Element.setHTMLUnsafe() et Document.parseHTMLUnsafe().

3. Analyser les entrées utilisateur avec Document.parseHTML()

Si vous devez analyser du code HTML non fiable sans l'insérer immédiatement dans le DOM actif, utilisez Document.parseHTML(). Cette méthode renvoie un Document qui a été nettoyé selon des règles par défaut ou personnalisées. Vous pouvez ainsi inspecter ou transformer les nœuds nettoyés (par exemple, pour créer un aperçu ou extraire du texte) avant de les insérer.

Déplacez les nœuds obtenus dans la page au lieu de les sérialiser en une chaîne et de les analyser à nouveau avec innerHTML, ce qui peut réintroduire une faille XSS de mutation (mXSS).

const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';

const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);

Solutions de remplacement et compatibilité des navigateurs avec l'API Sanitizer

Si l'API Sanitizer native n'est pas disponible dans vos navigateurs cibles, chargez une bibliothèque telle que DOMPurify uniquement lorsque la compatibilité native est manquante, afin d'éviter de l'envoyer aux navigateurs qui n'en ont pas besoin. Utilisez les vérifications in pour la détection des fonctionnalités afin que la vérification elle-même fonctionne dans les anciens navigateurs.

let domPurifyPromise;

function loadDOMPurify() {
  // Code-split with your bundler so DOMPurify is only downloaded when needed
  domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
  return domPurifyPromise;
}

export async function safeSetHTML(el, html) {
  const elements = ['p', 'b', 'i', 'strong', 'em'];
  const attributes = ['class'];

  if ('setHTML' in Element.prototype) {
    // setHTML() accepts a plain configuration object as the sanitizer
    el.setHTML(html, { sanitizer: { elements, attributes } });
  } else {
    const DOMPurify = await loadDOMPurify();
    el.innerHTML = DOMPurify.sanitize(html, {
      ALLOWED_TAGS: [...elements, '#text'],
      ALLOWED_ATTR: attributes,
      // Match the native Sanitizer: strip aria-*/data-* and disallowed element content
      ALLOW_ARIA_ATTR: false,
      ALLOW_DATA_ATTR: false,
      KEEP_CONTENT: false
    });
  }
}

Par défaut, DOMPurify conserve les attributs aria-* et data-* (même lorsque ALLOWED_ATTR est spécifié) ainsi que le contenu textuel des éléments qu'il supprime, tandis que le nettoyeur natif supprime les attributs aria-* et data-*, et supprime les éléments non autorisés ainsi que leur contenu. En définissant ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false et en autorisant #text, DOMPurify correspond au comportement natif. DOMPurify ne dispose pas d'une option intégrée équivalente à replaceWithChildrenElements. Évitez donc cette option dans les configurations qui nécessitent une solution de remplacement correspondante.

Si la page applique les types de confiance (require-trusted-types-for 'script'), l'attribution d'une chaîne à innerHTML génère une erreur. setHTML() n'a pas besoin d'une règle Trusted Types, mais la création de remplacement en a besoin : transmettez RETURN_TRUSTED_TYPE: true à DOMPurify.sanitize() et autorisez son nom de règle dompurify dans votre directive CSP trusted-types.