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

La nouvelle API Sanitizer vise à créer un processeur robuste pour insérer des chaînes arbitraires en toute sécurité dans une page.

Jack J
Jack J

Les applications traitent constamment des chaînes non fiables, mais il peut s'avérer délicat d'afficher ce contenu de manière sécurisée dans un document HTML. Sans une diligence suffisante, il est facile de créer accidentellement des opportunités de script intersites (XSS) pouvant être exploitées par des pirates informatiques.

Pour atténuer ce risque, la nouvelle proposition d'API Sanitizer vise à créer un processeur robuste pour insérer des chaînes arbitraires en toute sécurité dans une page. Cet article présente l'API et explique son utilisation.

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

Échappement de l'entrée utilisateur

Lorsque vous insérez une entrée utilisateur, des chaînes de requête, le contenu des cookies, etc., dans le DOM, les chaînes doivent être correctement échappées. Une attention particulière doit être portée à la manipulation DOM via .innerHTML, où les chaînes sans échappement sont une source typique de 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 si vous la développez à l'aide de .textContent, alert(0) ne sera pas exécuté. Toutefois, comme <em> ajouté par l'utilisateur est également développé sous la forme d'une chaîne, cette méthode ne peut pas être utilisée pour conserver la décoration de texte en HTML.

La meilleure chose à faire dans ce cas n'est pas d'échapper, mais de nettoyer.

Nettoyer les entrées utilisateur

Différence entre échappement et désinfection

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

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

Exemple

Dans l'exemple précédent, <img onerror> entraîne l'exécution du gestionnaire d'erreurs, mais si le gestionnaire onerror a été supprimé, il est possible de le développer en toute sécurité 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 nettoyer correctement la chaîne, il est nécessaire d'analyser la chaîne d'entrée au format HTML, d'omettre les balises et les attributs considérés comme dangereux, et de conserver ceux qui sont inoffensifs.

La spécification d'API Sanitizer proposée vise à proposer ce type de traitement sous la forme d'une 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>

Cependant, { sanitizer: new Sanitizer() } est l'argument par défaut. Cela peut se présenter comme suit.

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

Notez que setHTML() est défini sur Element. En tant que méthode de Element, le contexte à analyser est explicite (<div> dans ce cas). L'analyse est effectuée une 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éclenchent l'exécution du script. Cependant, 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 la manière dont le résultat de la désinfection doit traiter l'élément spécifié.

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

blockElements: noms des éléments que le désinfectant doit supprimer, tout en conservant les enfants.

dropElements: noms des éléments que le désinfectant 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 désinfectant 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 correspondance d'attributs, c'est-à-dire des objets dont les clés sont des noms d'attributs et les valeurs sont des listes d'éléments cibles ou le 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, les autres configurations des éléments et attributs continuent de s'appliquer.

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 propose une fonctionnalité de nettoyage. 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 remplacement 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 fait perdre du temps de traitement, mais peut également entraîner des vulnérabilités intéressantes causées par des cas où le résultat de la deuxième analyse est différent de celui de la première.

HTML nécessite également 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 comme argument, le contexte d'analyse doit être deviné.

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

État de l'API et compatibilité du navigateur

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

Step État
1. Créer une vidéo explicative Fin
2. Créer un brouillon de spécification Fin
3. Recueillir des commentaires et itérer sur la conception Fin
4. Phase d'évaluation de Chrome Fin
5. lancement Intention d'expédition sur M105

Mozilla: considère que cette proposition mérite d'être prototypée et s'engage activement à la mettre en œuvre.

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

Activer l'API Sanitizer

Navigateurs pris en charge

  • x
  • x
  • x

Source

Activation via l'option about://flags ou 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 dans les versions en 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 si vous avez des commentaires, n'hésitez pas à nous en faire part. Faites-nous part de vos commentaires sur les problèmes GitHub liés à l'API Sanitizer, et discutez-en avec les auteurs des spécifications et les personnes intéressées par cette API.

Si vous constatez des bugs ou des comportements inattendus dans l'implémentation de Chrome, signalez un bug. Sélectionnez les composants Blink>SecurityFeature>SanitizerAPI et partagez les détails pour aider les responsables de la mise en œuvre à suivre le problème.

Démonstration

Pour voir l'API Sanitizer en action, consultez le Sanitizer API Playground de Mike West:

Références


Photo de Towfiqu barbhuiya sur Unsplash.