Manipulation DOM sécurisée avec l'API Sanitizer

La nouvelle API Sanitizer vise à créer un processeur robuste permettant d'insérer des chaînes arbitraires de manière sécurisée dans une page.

Jack J
Jack J

Les applications traitent constamment des chaînes non approuvées, mais l'affichage sécurisé de ce contenu dans un document HTML peut s'avérer délicat. Sans précautions suffisantes, il est facile de créer accidentellement des opportunités de script intersites (XSS) que des pirates informatiques malveillants peuvent exploiter.

Pour atténuer ce risque, la nouvelle proposition d'API Sanitizer vise à créer un processeur robuste permettant d'insérer de manière sécurisée des chaînes arbitraires dans une page. Cet article présente l'API et explique comment l'utiliser.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

Échapement de l'entrée utilisateur

Lorsque vous insérez des entrées utilisateur, des chaînes de requête, le contenu de cookies, etc. dans le DOM, les chaînes doivent être correctement échappées. Une attention particulière doit être accordée à la manipulation du DOM via .innerHTML, où les chaînes non échappées sont une source typique d'attaque XSS.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

Si vous échappez les caractères spéciaux HTML dans la chaîne d'entrée ci-dessus ou l'étendez à l'aide de .textContent, alert(0) ne sera pas exécuté. Toutefois, comme le <em> ajouté par l'utilisateur est également développé en tant que chaîne, cette méthode ne peut pas être utilisée pour conserver la décoration du texte en HTML.

La meilleure solution ici n'est pas d'échapper, mais de nettoyer.

Nettoyer les entrées utilisateur

Différence entre l'échappement et la validation

L'échappement consiste à remplacer les caractères HTML spéciaux par des entités HTML.

La validation consiste à supprimer les parties sémantiquement dangereuses (telles que l'exécution de script) des chaînes HTML.

Exemple

Dans l'exemple précédent, <img onerror> entraîne l'exécution du gestionnaire d'erreurs, mais si le gestionnaire onerror était supprimé, il serait possible de l'étendre de manière sécurisée dans le DOM tout en laissant <em> intact.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Pour effectuer une bonne purification, il est nécessaire d'analyser la chaîne d'entrée en tant que code HTML, d'omettre les balises et les attributs considérés comme dangereux, et de conserver les éléments inoffensifs.

La spécification proposée de l'API Sanitizer vise à fournir ce traitement en tant qu'API standard pour les navigateurs.

API Sanitizer

L'API Sanitizer est utilisée de la manière suivante:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

Toutefois, { sanitizer: new Sanitizer() } est l'argument par défaut. Il peut donc être comme ci-dessous.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

Notez que setHTML() est défini sur Element. Étant donné qu'il s'agit d'une méthode de Element, le contexte à analyser s'explique de lui-même (<div> dans ce cas). L'analyse est effectuée une seule fois en interne, et le résultat est directement développé dans le DOM.

Pour obtenir le résultat du nettoyage sous forme de chaîne, vous pouvez utiliser .innerHTML à partir des résultats setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Personnaliser via la configuration

L'API Sanitizer est configurée par défaut pour supprimer les chaînes qui déclencheraient l'exécution du script. Toutefois, vous pouvez également ajouter vos propres personnalisations au processus de nettoyage via un objet de configuration.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

Les options suivantes spécifient comment le résultat de la purification doit traiter l'élément spécifié.

allowElements: noms des éléments que le nettoyeur doit conserver.

blockElements: noms des éléments que le nettoyeur doit supprimer, tout en conservant leurs enfants.

dropElements: noms des éléments que le nettoyeur doit supprimer, ainsi que leurs enfants.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

Vous pouvez également contrôler si le nettoyeur autorise ou refuse les attributs spécifiés à l'aide des options suivantes:

  • allowAttributes
  • dropAttributes

Les propriétés allowAttributes et dropAttributes requièrent des listes de correspondances d'attributs, des objets dont les clés sont des noms d'attributs et les valeurs des listes d'éléments cibles ou du caractère générique *.

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements permet d'autoriser ou de refuser les éléments personnalisés. Si elles sont autorisées, d'autres configurations pour les éléments et les attributs s'appliquent toujours.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

Surface d'API

Comparaison avec DomPurify

DOMPurify est une bibliothèque bien connue qui offre une fonctionnalité de désinfection. La principale différence entre l'API Sanitizer et DOMPurify est que DOMPurify renvoie le résultat de la désinfection sous forme de chaîne, que vous devez écrire dans un élément DOM via .innerHTML.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify peut servir de solution de secours lorsque l'API Sanitizer n'est pas implémentée dans le navigateur.

L'implémentation de DOMPurify présente quelques inconvénients. Si une chaîne est renvoyée, la chaîne d'entrée est analysée deux fois, par DOMPurify et .innerHTML. Cette double analyse gaspille du temps de traitement, mais peut également entraîner des failles intéressantes lorsque le résultat de la deuxième analyse est différent du premier.

Le code HTML a également besoin d'un contexte pour être analysé. Par exemple, <td> a du sens dans <table>, mais pas dans <div>. Étant donné que DOMPurify.sanitize() n'accepte qu'une chaîne en tant qu'argument, le contexte d'analyse devait être deviné.

L'API Sanitizer améliore l'approche DOMPurify et est conçue pour éliminer le besoin d'une double analyse et clarifier le contexte d'analyse.

État de l'API et compatibilité avec les navigateurs

L'API Sanitizer est en cours de discussion dans le processus de normalisation, et Chrome est en train de l'implémenter.

Étape État
1. Créer un message d'explication Fin
2. Créer un brouillon de spécification Fin
3. Recueillir des commentaires et itérer sur la conception Fin
4. Essai Origin Trial de Chrome Fin
5. Lancer Intent to Ship sur M105

Mozilla: considère cette proposition comme méritant d'être prototypée et l'implémente activement.

WebKit: consultez la réponse sur la liste de diffusion WebKit.

Activer l'API Sanitizer

Activation via about://flags ou option de la CLI

Chrome

Chrome est en train d'implémenter l'API Sanitizer. Dans Chrome 93 ou version ultérieure, vous pouvez tester ce comportement en activant l'indicateur about://flags/#enable-experimental-web-platform-features. Dans les versions antérieures de Chrome Canary et du canal de développement, vous pouvez l'activer via --enable-blink-features=SanitizerAPI et l'essayer dès maintenant. Consultez les instructions pour exécuter Chrome avec des indicateurs.

Firefox

Firefox implémente également l'API Sanitizer en tant que fonctionnalité expérimentale. Pour l'activer, définissez l'indicateur dom.security.sanitizer.enabled sur true dans about:config.

Détection de fonctionnalités

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Commentaires

Si vous essayez cette API et que vous avez des commentaires à nous faire, nous serions ravis de les recevoir. Partagez vos commentaires sur les problèmes GitHub de l'API Sanitizer et discutez avec les auteurs de la spécification et les personnes intéressées par cette API.

Si vous constatez des bugs ou un comportement inattendu dans l'implémentation de Chrome, signalez-les. Sélectionnez les composants Blink>SecurityFeature>SanitizerAPI et partagez des informations pour aider les implémentateurs à suivre le problème.

Démo

Pour voir l'API Sanitizer en action, consultez le bac à sable de l'API Sanitizer de Mike West:

Références


Photo de Towfiqu barbhuiya sur Unsplash.