Le WordPress d'aujourd'hui, décodé pour les développeurs

Sécurité

Assainir une sortie HTML dynamique sans expression régulière fragile

Filtrer du HTML avec une expression régulière semble rapide, jusqu'au jour où une balise imbriquée passe au travers. L'API DOM native évite ce piège.

Par WordPress Développement • 20 mars 2023 • 4 min de lecture • Aucun commentaire
Assainir une sortie HTML dynamique sans expression régulière fragile

« Une expression régulière ne peut pas analyser du HTML de façon fiable » : cette affirmation, souvent citée sans être comprise, se vérifie très concrètement dès qu’un contenu externe contient une balise imbriquée ou mal fermée. Un développeur confronté à ce problème sur un flux HTML provenant d’une API tierce en a fait l’expérience directe.

Le site affichait des extraits d’articles partenaires récupérés via une API externe, rendus tels quels dans une page WordPress après un simple preg_replace() censé retirer les balises <script>. La méthode fonctionnait, jusqu’à ce qu’un des partenaires renvoie une balise <scr<script>ipt>, syntaxiquement absurde pour un humain mais parfaitement interprétée par un navigateur une fois la première occurrence de <script> retirée par la regex, laissant réapparaître une balise valide.

Pourquoi une regex échoue structurellement sur du HTML

Une expression régulière traite un flux de caractères, sans notion d’imbrication, de fermeture de balise ou d’échappement contextuel. Elle peut repérer un motif, mais ne peut pas raisonner sur la structure arborescente d’un document. C’est précisément cette structure qu’un attaquant exploite : en insérant des fragments qui, une fois qu’une partie a été retirée, recomposent une balise dangereuse.

Ce n’est pas un défaut d’implémentation corrigible en ajoutant une regex plus complexe. C’est une limite de fond de l’outil lui-même face à un langage qui n’est pas régulier au sens formel du terme. La solution ne consiste donc pas à améliorer la regex, mais à changer d’outil.

Construire un arbre plutôt que chercher un motif

DOMDocument, disponible nativement en PHP, charge un fragment HTML en un arbre de nœuds réellement structuré. Une balise et son contenu deviennent un nœud unique, quelle que soit la façon dont elle a été écrite dans le flux d’origine. Retirer une balise revient alors à retirer un nœud entier de l’arbre, une opération qui ne peut pas être trompée par un jeu d’imbrication de caractères.

function assainir_extrait_partenaire( string $html ) : string {
    $dom = new DOMDocument();
    // On force l'encodage UTF-8 et on ignore les avertissements de structure incomplète.
    @$dom->loadHTML(
        '<?xml encoding="utf-8" ?>' . $html,
        LIBXML_NOERROR | LIBXML_NOWARNING
    );

    $balises_interdites = array( 'script', 'style', 'iframe', 'object', 'embed', 'form' );
    foreach ( $balises_interdites as $nom_balise ) {
        $noeuds = $dom->getElementsByTagName( $nom_balise );
        // On collecte avant de supprimer : la liste change pendant qu'on itère.
        $a_supprimer = array();
        foreach ( $noeuds as $noeud ) {
            $a_supprimer[] = $noeud;
        }
        foreach ( $a_supprimer as $noeud ) {
            $noeud->parentNode->removeChild( $noeud );
        }
    }

    return $dom->saveHTML( $dom->documentElement );
}

La collecte préalable des nœuds avant suppression n’est pas un détail : DOMNodeList reste connectée en direct à l’arbre, et supprimer un nœud pendant l’itération décale les index restants, ce qui peut faire sauter des occurrences sans qu’aucune erreur ne le signale.

L'essentiel à retenir : Une regex ne comprend jamais vraiment la structure d'un document HTML ; DOMDocument analyse un arbre, pas une suite de caractères ; Retirer un nœud entier plutôt que masquer une chaîne de texte

Traiter aussi les attributs, pas seulement les balises

Retirer des balises entières ne suffit pas si des attributs comme onerror ou onclick subsistent sur des balises par ailleurs autorisées, telles qu’une simple <img>. L’arbre DOM permet ici aussi un contrôle fin, balise par balise et attribut par attribut.

foreach ( $dom->getElementsByTagName( '*' ) as $element ) {
    foreach ( iterator_to_array( $element->attributes ) as $attribut ) {
        if ( 0 === stripos( $attribut->name, 'on' ) ) {
            $element->removeAttribute( $attribut->name );
        }
    }
}

Les limites à connaître avant d’adopter cette approche

DOMDocument reste plus coûteux en temps de traitement qu’une simple regex, ce qui compte sur un flux volumineux traité à chaque affichage plutôt qu’en tâche de fond. Il est également nécessaire de mettre en cache le résultat assaini, via un transitoire ou une table dédiée, plutôt que de relancer l’analyse à chaque chargement de page.

  • Toujours désactiver les entités externes avant de charger un contenu non fiable, pour éviter une attaque XXE.
  • Mettre en cache le résultat assaini plutôt que de retraiter le même contenu à chaque requête.
  • Documenter la liste des balises et attributs autorisés comme une politique explicite, pas comme un oubli implicite.

En résumé

Une regex peut suffire pour un motif simple et prévisible, mais jamais pour assainir un document HTML entier destiné à être rendu par un navigateur. L’API DOM native de PHP, en construisant un arbre réel plutôt qu’en cherchant un motif dans une chaîne de caractères, élimine par construction toute la classe de contournements fondés sur l’imbrication ou la fragmentation de balises.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi