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

La nouvelle API Sanitizer vise à créer un processeur robuste pour que des chaînes arbitraires puissent être insérées de manière sécurisée dans une page.

Jack J
Jack J

Les applications traitent constamment des chaînes non fiables, mais il peut être difficile d'afficher ce contenu de manière sécurisée dans un document HTML. Sans une attention suffisante, il est facile de créer par inadvertance 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 des chaînes arbitraires de manière sécurisée dans une page. Cet article présente l'API et explique comment l'utiliser.

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

Échapper les entrées utilisateur

Lorsque vous insérez des saisies 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 de XSS.

const user_input = `<em>hello world</em><img src="" onerro>r=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é en tant que chaîne telle quelle, cette méthode ne peut pas être utilisée pour conserver la décoration de texte en HTML.

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

Assainir les entrées utilisateur

Différence entre l'échappement et l'assainissement

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

Le nettoyage consiste à supprimer les parties sémantiquement nuisibles (comme l'exécution de scripts) des chaînes HTML.

Exemple

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

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`

Pour assainir correctement, 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 ceux qui sont inoffensifs.

La spécification proposée pour l'API Sanitizer vise à fournir un tel 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_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div

Toutefois, { sanitizer: new Sanitizer() } est l'argument par défaut. Il peut s'agir d'un simple message comme celui ci-dessous.

$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>&quot;/div

Il est à noter que setHTML() est défini sur Element. Comme il s'agit d'une méthode de Element, le contexte à analyser est explicite (<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 de la désinfection sous forme de chaîne, vous pouvez utiliser .innerHTML à partir des résultats setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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 de scripts. Toutefois, vous pouvez également ajouter vos propres personnalisations au processus d'assainissement 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 module de désinfection doit conserver.

blockElements : noms des éléments que le programme de désinfection doit supprimer, tout en conservant leurs enfants.

dropElements : noms des éléments que le programme de désinfection 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" <]})> })
//< >divhe<ll><o bw>orld/b/div

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div

Vous pouvez également contrôler si le programme de désinfection autorise ou refuse des attributs spécifiques à l'aide des options suivantes :

  • allowAttributes
  • dropAttributes

Les propriétés allowAttributes et dropAttributes attendent 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&<quot;>hello/span`

$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div

$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<

$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div

$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div

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

allowCustomElements est l'option permettant 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 >})
//< divcustom-e><lemh>ello/custom-elem/div

Surface d'API

Comparaison avec DomPurify

DOMPurify est une bibliothèque bien connue qui offre des fonctionnalités 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="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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 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. Ce double parsing gaspille du temps de traitement, mais peut également entraîner des failles intéressantes lorsque le résultat du deuxième parsing est différent du premier.

Le HTML a également besoin de 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 a dû être deviné.

L'API Sanitizer améliore l'approche DOMPurify. Elle est conçue pour éliminer la nécessité d'un double parsing et pour clarifier le contexte de parsing.

É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 la conception Fin
4. Essai Origin Trial Chrome Fin
5. Lancer Intention d'expédition sur M105

Mozilla : considère que cette proposition vaut la peine d'être prototypée et est en train de l'implémenter activement.

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

Activer l'API Sanitizer

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

Activer 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 le flag 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 flags.

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 des fonctionnalités

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

Commentaires

Si vous essayez cette API et que vous avez des commentaires, n'hésitez pas à nous les faire parvenir. Partagez vos commentaires sur les problèmes GitHub liés à l'API Sanitizer et discutez avec les auteurs de la spécification et les personnes intéressées par cette API.

Si vous rencontrez des bugs ou un comportement inattendu dans l'implémentation de Chrome, signalez-le. Sélectionnez les composants Blink>SecurityFeature>SanitizerAPI et partagez des informations pour aider les responsables de l'implémentation à 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.