Attaque XXE : entités externes XML, lecture de fichiers et SSRF
Section intitulée « Attaque XXE : entités externes XML, lecture de fichiers et SSRF »Un analyseur XML conforme à la spécification fait exactement ce qu’on lui demande : il résout les entités déclarées dans le document, y compris celles qui pointent vers un fichier local ou une adresse réseau. L’attaque XXE (XML External Entity) exploite cette obéissance pour lire des fichiers du serveur et émettre des requêtes vers l’infrastructure interne.
Imagine un formulaire qu’on remplit, mais où certaines cases disent « recopie ici le contenu de tel tiroir » au lieu d’un texte. Si l’employé obéit sans vérifier, on lui fait recopier des tiroirs qui ne nous regardent pas. L’attaque XXE joue exactement sur cette obéissance aveugle d’un lecteur de documents.
→ Lire l’explication complète de la marmotte
DTD, entités générales et entités de paramètre : pourquoi l’analyseur obéit
Section intitulée « DTD, entités générales et entités de paramètre : pourquoi l’analyseur obéit »Un document XML peut embarquer une DTD (Document Type Definition), un en-tête qui déclare la structure attendue et, au passage, des entités. Une entité est un raccourci : &nom; dans le corps du document se fait remplacer par la valeur déclarée. Rien de suspect tant que la valeur est un bout de texte.
Le problème commence avec les entités externes. La norme XML 1.0 autorise une déclaration du type <!ENTITY xxe SYSTEM "file:///etc/passwd">. Le mot-clé SYSTEM désigne une ressource par son URI, PUBLIC ajoute un identifiant public avant l’URI. À la résolution, l’analyseur va chercher le contenu pointé et l’injecte à la place de &xxe;. Un analyseur strictement conforme fait ça par défaut, parce que la spécification le lui demande. La conformité, ici, est le bug, et il porte un numéro : CWE-611, restriction insuffisante des références d’entités externes XML.
On distingue deux familles d’entités. Les entités générales (&xxe;) apparaissent dans le corps du document. Les entités de paramètre (%xxe;), elles, vivent dans la DTD elle-même et servent à construire d’autres déclarations : c’est le vecteur des variantes aveugles et hors bande, parce qu’elles permettent de composer dynamiquement une requête d’exfiltration sans jamais afficher le résultat dans la réponse.
Le schéma ci-dessous montre qui résout quoi quand un document piégé arrive.
Ce qu’il faut lire : l’analyseur agit pour le compte du service, avec ses droits et depuis sa position réseau. C’est précisément ce qui rend l’attaque intéressante côté offensif, et pénible côté défense.
XXE comme lecture de fichiers et comme vecteur de SSRF
Section intitulée « XXE comme lecture de fichiers et comme vecteur de SSRF »Le premier usage est la lecture de fichiers locaux. Une entité SYSTEM "file:///etc/passwd" reflétée dans la réponse livre le fichier. Les cibles classiques : fichiers de configuration, clés privées, variables d’environnement exposées via /proc, jetons de service. Sur ce dernier point, la mécanique rejoint celle décrite pour le vol de jeton de service account Kubernetes quand le jeton traîne sur le disque du pod.
Le second usage est plus large : l’URI d’une entité peut être http:// autant que file://. L’analyseur devient alors un client HTTP qui émet une requête depuis l’intérieur du périmètre. C’est une SSRF (Server-Side Request Forgery) à part entière, et XXE en est un vecteur commode. La cible typique : les services de métadonnées des fournisseurs cloud, dont la chaîne complète est décrite pour la SSRF vers IMDS et le vol d’identifiants IAM, les API internes sans authentification réseau, les interfaces d’administration qui ne s’attendaient qu’à du trafic localhost. Un analyseur XML qui parle à http://169.254.169.254/ au nom du service, c’est un portier qui ouvre aux coursiers sans regarder le badge.
Deux précisions honnêtes sur les cas difficiles. D’abord le XXE aveugle : quand la réponse ne reflète rien, on ne voit pas le contenu lu. On s’en sort par exfiltration hors bande, avec une DTD externe et des entités de paramètre qui construisent une URL contenant le fichier lu, puis déclenchent une requête vers un serveur contrôlé par l’attaquant. Ensuite les canaux d’erreur : forcer l’analyseur à inclure une valeur dans un message d’erreur (un chemin de fichier invalide construit à partir du contenu lu) fait fuiter l’information ligne par ligne. Ces variantes dépendent toutes de la résolution des entités de paramètre et du chargement de DTD externes, ce qui désigne directement la contre-mesure.
Désactiver la résolution des entités externes : le réglage par pile
Section intitulée « Désactiver la résolution des entités externes : le réglage par pile »La position est tranchée : un service qui parse du XML non fourni par lui-même doit désactiver les entités externes et le chargement des DTD, point. Il n’existe presque aucun format d’échange légitime qui dépende de la résolution d’entités externes. Le compromis « on garde les DTD au cas où » ne gagne rien et paie en surface d’attaque. La seule vraie question : quel réglage sur quelle bibliothèque, parce que chaque pile a sa syntaxe et ses pièges. L’aide-mémoire XML External Entity Prevention de l’OWASP les recense pile par pile ; ce qui suit en garde les réglages qui tiennent.
Java expose plusieurs fabriques (DocumentBuilderFactory, SAXParserFactory, XMLInputFactory). Le réglage fiable active disallow-doctype-decl, qui rejette tout document portant une DTD :
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();// Rejette tout document contenant une DTD : coupe XXE à la racine.dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);// Ceinture et bretelles au cas où une DTD serait tolérée ailleurs.dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);dbf.setXIncludeAware(false);dbf.setExpandEntityReferences(false);Python avec la bibliothèque standard : xml.etree.ElementTree ne résout pas les entités externes (il refuse le document sur une entité non déclarée), mais xml.sax et lxml sont une autre histoire. Pour lxml, il faut un XMLParser explicitement verrouillé :
from lxml import etree# no_network coupe les accès réseau, resolve_entities désactive l'expansion.parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False, dtd_validation=False)tree = etree.parse(source, parser)Sur .NET, XmlReaderSettings avec DtdProcessing = DtdProcessing.Prohibit ferme la porte ; les versions récentes du framework mettent déjà Prohibit ou Ignore par défaut, mais un XmlDocument ancien avec un XmlResolver non nul reste vulnérable. En PHP, libxml sous le capot : libxml_set_external_entity_loader(null) et, sur les versions antérieures à PHP 8.0, libxml_disable_entity_loader(true) avant le parsing. Depuis libxml2 2.9 (septembre 2012), le chargement des entités externes et des DTD réseau est désactivé par défaut, ce qui couvre une partie du parc, mais une application qui réactive la substitution des entités (LIBXML_NOENT) en autorisant le chargement des DTD (LIBXML_DTDLOAD) rouvre explicitement le trou. L’option LIBXML_NOENT est un piège de nommage : contrairement à ce que son nom suggère, elle n’enlève pas les entités, elle les fait substituer.
Le tableau mental à retenir : les bibliothèques récentes corrigent les défauts d’usine, mais chaque flag passé « parce qu’un tutoriel le disait » peut les réactiver. Auditer les appels, pas seulement les versions.
Exemple : point d’entrée qui lit un fichier, puis frappe l’interne
Section intitulée « Exemple : point d’entrée qui lit un fichier, puis frappe l’interne »Soit un service d’import qui accepte un document de configuration en XML. L’ouvrage monté pour la démonstration reflète dans sa réponse le champ <nom> du document. Le premier document lit un fichier local, le second détourne l’analyseur vers une adresse interne.
<?xml version="1.0"?><!DOCTYPE config [ <!ENTITY secret SYSTEM "file:///etc/hostname">]><config><nom>&secret;</nom></config><!-- La réponse renvoie le hostname : lecture de fichier confirmée. --><?xml version="1.0"?><!DOCTYPE config [ <!ENTITY probe SYSTEM "http://192.0.2.10:8080/admin/status">]><config><nom>&probe;</nom></config><!-- L'analyseur émet la requête depuis le réseau du service : SSRF. -->Le second document ne vise ici qu’une adresse de la plage de documentation 192.0.2.0/24. Dans un environnement réel, la même structure atteindrait un service de métadonnées ou une API interne. La contre-mesure testée (disallow-doctype-decl côté Java, parser verrouillé côté lxml) fait échouer le parsing au premier <!DOCTYPE, avant toute résolution : c’est le comportement attendu d’un analyseur durci.
Formats bureautiques et images vectorielles : la surface qu’on oublie
Section intitulée « Formats bureautiques et images vectorielles : la surface qu’on oublie »Le piège le plus coûteux n’est pas l’API SOAP (Simple Object Access Protocol) qu’on sait pleine de XML. Ce sont les formats qui embarquent du XML sans le dire. Les documents bureautiques .docx, .xlsx, .pptx sont des archives ZIP contenant des fichiers XML ; les images vectorielles SVG (Scalable Vector Graphics) sont du XML. Tout service qui accepte un upload de document, génère un aperçu de SVG, ou extrait des métadonnées d’un fichier Office, passe potentiellement ce XML dans un analyseur. Un convertisseur d’images qui rend un SVG fourni par l’utilisateur est un point d’entrée XXE aussi franc qu’une API, et la logique rejoint celle de l’upload de shell déguisé : le danger est dans le traitement, pas dans l’extension.
Vérifier qu’un analyseur est durci se fait par un test positif et un test négatif. Test négatif : envoyer un document portant une DTD et une entité file:// vers une ressource connue, confirmer que le parsing échoue ou que l’entité n’est pas résolue. Test positif : confirmer qu’un document XML légitime sans DTD passe toujours. Un durcissement qui casse le trafic normal se fait désactiver en catastrophe le lundi suivant, ce qui revient à ne pas l’avoir fait.
Ce que la désactivation ne garantit pas
Section intitulée « Ce que la désactivation ne garantit pas »Couper les entités externes supprime le vecteur XXE, rien de plus. Le filtrage des flux sortants du service reste nécessaire comme seconde couche : une SSRF peut venir d’ailleurs que du XML (une URL fournie par l’utilisateur, un webhook mal vérifié comme décrit pour les webhooks non vérifiés). Un service qui n’a aucune raison métier d’atteindre le service de métadonnées cloud ou l’interface d’administration interne doit en être empêché au niveau réseau, indépendamment de l’analyseur.
Quand le format d’échange est négociable, préférer un format sans entités : JSON ne connaît pas la notion d’entité externe et retire tout le problème. Remplacer XML par JSON pour une API interne est souvent le durcissement le moins coûteux, parce qu’il supprime la cause au lieu de la contenir. Ce raisonnement vaut aussi pour les attaques par désérialisation non sécurisée : le format le plus bavard est aussi le plus dangereux.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Un analyseur XML laissé dans sa configuration conforme transforme chaque point d’entrée acceptant du XML en primitive de lecture de fichiers. Les fichiers de configuration, clés privées et jetons de service posés sur le disque deviennent lisibles à distance, sans authentification autre que l’accès au formulaire d’import. Le même analyseur émet des requêtes vers le réseau interne au nom du service, atteignant les services de métadonnées cloud et les API sans contrôle d’origine, ce qui ouvre une progression latérale. Les variantes aveugles exfiltrent par DTD externe ou par messages d’erreur, sans trace exploitable dans les journaux applicatifs. Les uploads de documents bureautiques et d’images SVG passent inaperçus comme vecteurs, puisqu’on ne les perçoit pas comme du XML.
- Extensible Markup Language (XML) 1.0 (Fifth Edition), W3C, 26 novembre 2008.
- XML External Entity Prevention Cheat Sheet, OWASP.
- CWE-611, Improper Restriction of XML External Entity Reference, MITRE.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Le XML est une façon d’écrire des documents que les programmes savent lire : des balises qui encadrent des valeurs. Dans ce format, on peut définir des raccourcis : on écrit un nom court, et le lecteur le remplace par un texte plus long. Jusqu’ici, rien de dangereux. Le piège, c’est qu’un de ces raccourcis a le droit de dire « remplace-moi par le contenu de ce fichier » ou « par ce que renvoie cette adresse internet ». Et le lecteur de documents, par défaut, obéit : il va vraiment ouvrir le fichier ou se connecter à l’adresse, parce que la règle officielle du format le lui demande.
Un attaquant qui peut envoyer un document à un serveur écrit donc un raccourci pointant vers un fichier secret du serveur, puis fait afficher ce raccourci. Le serveur recopie le fichier dans sa réponse et le livre. Même astuce avec une adresse réseau : le serveur se connecte tout seul à des machines internes qu’un visiteur extérieur ne pourrait jamais joindre. Si la réponse ne montre rien, il existe des variantes plus retorses qui font sortir l’information par d’autres chemins.
La défense tient en une phrase : configurer le lecteur de documents pour qu’il refuse ces raccourcis vers l’extérieur, voire qu’il rejette tout document qui en contient. Beaucoup de bibliothèques récentes le font déjà d’elles-mêmes, mais une option mal choisie peut rouvrir la porte. Quand c’est possible, mieux vaut utiliser un autre format, comme le JSON, qui ne connaît pas ces raccourcis du tout.
À lire aussi
- Cross-site scripting (XSS) : mécanisme, variantes et défensesComprendre le cross-site scripting (XSS), ses variantes réfléchie, stockée, DOM et par mutation, et les défenses qui tiennent réellement en production.
- CWE-918 : falsification de requête côté serveur (SSRF)CWE-918, server-side request forgery : ce que la classe recouvre, pourquoi son parent est le « député confus » CWE-441, ce qu'elle vise dans le nuage, et son rang 22 au Top 25 2025.
- Attaque par désérialisation YAML non sécuriséeComprendre et prévenir l'exécution de code provoquée par les tags de construction d'objets dans des parseurs YAML permissifs.
- Comment bloquer les requêtes côté serveur et la désérialisationSSRF et désérialisation permettent de détourner une application malgré un pare-feu correctement configuré.
Pages voisines
La citent
- CERT-FR alertes et avis de sécurité
- CWE-918 : falsification de requête côté serveur (SSRF)
- Signature XML et enveloppement : pourquoi une assertion SAML valide peut mentir
Attaque XXE : entités externes XML, lecture de fichiers et SSRF
Elle cite
