Sécuriser votre reading XML python : éviter les failles de parsing

On reçoit un flux XML depuis une API tierce, on appelle ET.parse(), le script tourne en production depuis six mois sans problème. Puis un jour, le serveur plante : mémoire saturée, CPU à fond, ou pire, un fichier local se retrouve exposé dans les logs. Le parsing XML en Python ouvre une surface d’attaque réelle, et la bibliothèque standard n’y fait pas barrage par défaut.

Attaques XML que votre parseur Python laisse passer

Quand on parse du XML non fiable avec xml.etree.ElementTree, on hérite des comportements permissifs de libexpat. Trois vecteurs reviennent systématiquement dans les incidents documentés.

Le premier, c’est l’attaque Billion Laughs (expansion récursive d’entités). Un document XML de quelques kilo-octets déclare des entités imbriquées qui, une fois résolues, génèrent un volume de données capable de saturer la mémoire du processus. Le parseur standard ne limite pas la profondeur de résolution.

Le deuxième vecteur, c’est le XXE (XML External Entity). Une entité externe pointe vers un fichier système (/etc/passwd, un fichier de configuration applicatif) ou vers une URL interne. Si le parseur résout cette entité, il renvoie le contenu du fichier dans la réponse ou dans les logs. La documentation officielle Python avertit elle-même que les modules XML ne sont pas protégés contre les données malicieuses.

Le troisième, moins connu, est le hash flooding XML. Des collisions massives de hash pendant le parsing provoquent une dégradation algorithmique qui transforme un simple appel parse() en déni de service CPU. Python 3.12.14 a d’ailleurs renforcé la protection contre ce type d’attaque dans xml.parsers.expat lorsque Python est compilé avec libExpat 2.8.0 ou supérieur.

Analyste en cybersécurité examinant des logs d'erreurs de parsing XML Python sur un poste de travail sécurisé

defusedxml : le garde-fou concret pour reading XML Python

La réponse la plus directe, c’est defusedxml. Ce paquet remplace les appels standard par des versions durcies qui désactivent la résolution d’entités externes, limitent l’expansion d’entités internes et bloquent les DTD distantes.

En pratique, on remplace un import :

  • import xml.etree.ElementTree as ET devient import defusedxml.ElementTree as ET, sans changer le reste du code. L’API reste identique, mais le parseur refuse les constructions dangereuses.
  • Pour minidom, même logique : defusedxml.minidom.parseString() remplace xml.dom.minidom.parseString().
  • Pour SAX, defusedxml.sax.parse() prend le relais avec les mêmes restrictions appliquées au handler.

Le surcoût en performance est négligeable sur des documents de taille normale. Adopter defusedxml prend cinq minutes et couvre la majorité des vecteurs d’attaque XML.

Cas où defusedxml ne suffit pas

Si on utilise lxml pour ses capacités XPath complètes ou XSLT, defusedxml ne couvre pas tous les scénarios. Les versions récentes de lxml (5.x et au-delà) ont durci leurs valeurs par défaut, mais on doit quand même vérifier explicitement que resolve_entities est à False dans le XMLParser.

Un piège courant : instancier un parseur lxml sans argument et supposer qu’il est sûr. Vérifiez toujours la configuration explicite du parseur lxml, même sur une version récente.

Vulnérabilité XPath dans ElementPath : un risque DoS récent

Au-delà des attaques classiques XXE et Billion Laughs, un CVE récent (CVE-2026-6879) a mis en lumière une vulnérabilité de complexité quadratique dans xml.etree.ElementPath. Certains prédicats XPath comme [1], [last()] ou [last()-N] déclenchent une consommation CPU disproportionnée lorsqu’ils sont appliqués à un document XML malicieusement construit.

Ce type d’attaque ne vole pas de données. Il vise le déni de service : le script qui parse des flux XML en boucle finit par monopoliser le CPU. Les correctifs ont été intégrés dans les distributions Python 3.13 pour certaines plateformes, mais si on travaille avec une version antérieure non patchée, on reste exposé.

La parade opérationnelle ici ne repose pas uniquement sur le choix du parseur. On ajoute des gardes au niveau applicatif :

  • Un timeout sur chaque opération de parsing, pour couper un traitement qui dépasse une durée raisonnable.
  • Une limite de taille sur le fichier XML accepté en entrée, avant même d’appeler le parseur.
  • Un contrôle de la complexité des expressions XPath si elles proviennent d’une source externe (ce qui devrait être évité autant que possible).

Deux ingénieurs logiciels discutant des bonnes pratiques de sécurisation du parsing XML en Python

Valider le XML en amont du parsing Python

Parser un fichier XML, c’est une chose. Mais valider sa structure avant de l’exploiter protège contre les données inattendues, pas seulement contre les attaques.

La validation par schéma XSD, avec lxml.etree.XMLSchema, permet de rejeter un document dont la structure ne correspond pas au contrat attendu. On définit les éléments autorisés, leurs types, leurs cardinalités. Un document qui embarque un nœud inattendu ou une entité non déclarée est rejeté avant que le code métier ne le touche.

Validation et sécurité combinées

La validation XSD ne remplace pas defusedxml. Elle complète la chaîne : d’abord on parse de manière sûre (entités désactivées, DTD bloquée), puis on valide la structure. L’ordre compte. Si on valide avec un parseur non sécurisé, l’attaque peut se déclencher pendant la phase de validation elle-même.

Un schéma XSD strict a aussi l’avantage de documenter le format attendu pour l’équipe. Quand un partenaire modifie son flux XML sans prévenir, la validation échoue proprement au lieu de produire des données corrompues en silence.

Configuration défensive en production pour le parsing XML

En environnement de production, la sécurité du reading XML Python ne se limite pas au choix de la bibliothèque. On applique des mesures à plusieurs niveaux.

Isoler le processus de parsing du reste de l’application limite la portée d’un éventuel déni de service. Un worker dédié, avec des limites mémoire et CPU définies au niveau du système (cgroups, conteneur), empêche un XML malveillant de faire tomber toute l’application.

Côté code, on s’assure que les erreurs de parsing remontent explicitement. Un bloc try/except trop large qui avale les ParseError masque les tentatives d’attaque. Loguer l’erreur avec le contexte (source du fichier, taille, heure) permet de détecter des patterns suspects.

Les retours varient sur la nécessité de scanner les XML avec un outil tiers avant parsing. Sur des flux à haut volume, le coût peut être prohibitif. Sur des imports ponctuels depuis des sources non maîtrisées, un passage par un validateur externe ajoute une couche de défense appréciable.

Le point à retenir : un parseur sécurisé, une validation de schéma, des limites système et une gestion d’erreurs explicite forment une chaîne. Retirer un maillon expose le suivant. Chaque couche compense les angles morts de la précédente, et c’est cette combinaison qui rend le parsing XML en Python réellement fiable en conditions réelles.