Picarones / picarones /formats /_xml_utils.py
Claude
fix(sprint-S1.4): forbid_dtd=True + tests d'attaque XXE/Billion Laughs/DTD
2905909 unverified
Raw
History Blame
2.14 kB
"""Parsing XML sécurisé — anti-XXE / Billion Laughs / DTD retrieval.
Helper transverse appliqué partout où Picarones parse du XML reçu
depuis une source externe (corpus uploadé via le web, manifeste
Gallica, ALTO produit par un module ``BaseModule`` tiers, etc.).
Délègue à :mod:`defusedxml` (dépendance dure du projet) qui durcit
le parser stdlib contre :
- **XXE** (``XML External Entity``) — résolution d'entités vers
des fichiers locaux ou des URL distantes.
- **Billion Laughs** — expansion exponentielle d'entités.
- **DTD retrieval** — fetch d'une DTD distante.
Discipline : tout module qui parse du XML doit utiliser
``safe_parse_xml`` plutôt que ``xml.etree.ElementTree.fromstring``
directement.
Module nommé avec un ``_`` initial : c'est un détail
d'implémentation du package ``formats`` ; les callers passent par
``picarones.formats.xml.safe_parse_xml`` (re-export public au
niveau du package).
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from typing import Optional
import defusedxml
import defusedxml.ElementTree as _SafeET
def safe_parse_xml(xml_bytes: bytes) -> Optional[ET.Element]:
"""Parse du XML en bloquant entités externes ET ``<!DOCTYPE>``.
Sprint S1.4 — durcissement : ``forbid_dtd=True`` ajouté en plus
des défauts ``defusedxml`` (``forbid_entities=True``,
``forbid_external=True``). Sans ``forbid_dtd``, un payload
``<?xml...?><!DOCTYPE root SYSTEM "http://attacker/evil.dtd">``
est accepté (le fetch est bloqué par ``forbid_external`` mais
le DOCTYPE traverse le parser). ALTO 4 et PAGE XML utilisent
``xmlns`` plutôt que DOCTYPE — le durcissement est sans
régression sur le corpus institutionnel.
Retourne ``None`` si le payload n'est pas un XML valide ou si
``defusedxml`` détecte une attaque
(``EntitiesForbidden``, ``ExternalReferenceForbidden``,
``DTDForbidden``, ``NotSupportedError``).
"""
try:
return _SafeET.fromstring(xml_bytes, forbid_dtd=True)
except (ET.ParseError, defusedxml.DefusedXmlException):
return None
__all__ = ["safe_parse_xml"]