Aller au contenu

Attaque XXE : entités externes XML, lecture de fichiers et SSRF

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.

→ 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.

Schéma de séquence : un client envoie au service un document XML portant une DTD interne, le service le confie à l'analyseur XML, qui lit la DTD, y trouve une déclaration ENTITY SYSTEM et, selon le préfixe de l'URI, ouvre un fichier local ou émet une requête vers le réseau interne ; le contenu lu remonte à l'analyseur, puis au service sous forme d'arbre du document à entité résolue, et repart au client dans la réponse.

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.

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.

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.

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

Pages voisines