flawopen.com/XML External Entities (XXE)/Python XML parser security
Is Python xml.etree.ElementTree safe from XXE?
For classic external-file and network XXE, Python’s built-in Expat-backed parsers do not access local files or open network connections by default. That does not make every XML workload safe: Expat before 2.7.2 can be vulnerable to entity-expansion and large-token denial of service. lxml defaults also vary by version. Pin and explicitly configure parsers, or use defusedxml for untrusted XML.
Three XML parser risks to separate
- XXE (CWE-611):
- An XML parser resolves an external entity supplied through attacker-controlled XML, potentially reading local files or making server-side requests.
- Entity expansion DoS:
- Nested or repeated entity references consume disproportionate CPU or memory while the parser expands them.
- Expat:
- The C parser used by Python’s built-in XML modules; runtime version can be checked with
pyexpat.EXPAT_VERSION. - DTD:
- A document type declaration can define entities and, depending on parser settings, request external resources.
Untrusted XML reaches a parser
An upload, SVG, SAML message, or integration payload crosses a trust boundary and is passed to an XML API.
Parser implementation and options decide behavior
The standard library uses Expat; lxml uses libxml2. External access, entity expansion, parser version, and flags determine which behaviors are reachable.
Resolution or expansion has an effect
If external resolution is enabled, a declared entity may request local or remote content. Vulnerable expansion or oversized tokens can instead consume CPU and memory.
Process permissions bound impact
File permissions and network egress bound XXE impact; input-size limits, patched parser versions, safe flags, and process isolation reduce exposure.
Python’s current XML security guidance says Expat does not access local files or create network connections by default. It also warns that Expat versions older than 2.7.2 may be vulnerable to billion-laughs, quadratic-blowup, large-token, or disproportionate-memory issues; inspect pyexpat.EXPAT_VERSION because Python may use bundled or system Expat. This is version-specific resource-exhaustion risk, not proof that current ElementTree resolves external entities.
# Standard-library external access is off by default, # but old Expat builds have resource-exhaustion risks. import xml.etree.ElementTree as ET from xml.parsers import expat print(expat.EXPAT_VERSION) # inspect the runtime parser root = ET.fromstring(untrusted_xml) # lxml before 5.0 defaulted resolve_entities=True. # Explicit unsafe settings illustrate the risky boundary. from lxml import etree parser = etree.XMLParser(resolve_entities=True, load_dtd=True) root = etree.fromstring(untrusted_xml, parser)
from defusedxml.ElementTree import fromstring
# Reject DTD declarations when the format does not require them.
root = fromstring(untrusted_xml, forbid_dtd=True)
# If lxml is required, set its security options explicitly.
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False,
no_network=True,
load_dtd=False,
huge_tree=False,
)
root = etree.fromstring(untrusted_xml, parser)
# Do not run XInclude on untrusted trees without a restricted loader.
Choose safe settings for the parser you actually deploy
For defusedxml.ElementTree, entity declarations and external references are forbidden by default; set forbid_dtd=True as well when the accepted format does not need DTDs. For lxml, explicitly set resolve_entities=False, load_dtd=False, no_network=True, and huge_tree=False; current XMLParser defaults use internal-only entity resolution, while versions before lxml 5.0 defaulted to resolving entities. Do not enable XInclude on untrusted trees unless its loader is restricted. Apply request/decompressed-size limits before parsing: parser flags do not bound every resource cost.
- ✓Inventory every XML entry point and parser: ElementTree/Expat, lxml/libxml2, SAX/DOM, SVG, SAML, archive imports, and any later XInclude step.
- ✓Use a supported Python build with Expat 2.7.2 or later where available; verify the runtime with
pyexpat.EXPAT_VERSIONinstead of assuming the bundled version. - ✓For untrusted XML, use
defusedxml; reject DTDs withforbid_dtd=Truewhen the schema does not require them. - ✓If using lxml, pin a maintained release and explicitly disable entity resolution, DTD loading, network access, and huge-tree mode; do not rely on changing defaults.
- ✓Cap compressed and decompressed input size, isolate parsing where impact warrants it, and test benign DTD/entity fixtures in CI without sending external requests.
Why parser defaults need version-aware review
“Python XML” is not one parser configuration. The standard library delegates to Expat, and interpreter builds can use bundled or system libraries. lxml wraps libxml2 and changed its entity-resolution default in version 5.0; wrappers and explicit parser options can alter behavior again. Record the actual Python, Expat, lxml, and libxml2 versions in deployment inventory and make security flags explicit at each untrusted-input call site.
Frequently asked questions
No. Python documents that Expat does not access local files or create network connections by default. Check the Expat version and distinguish classic external-entity access from resource-exhaustion issues in older parser builds.
Do not generalize across versions. Current lxml XMLParser defaults to internal-only entity resolution, no DTD loading, and no network access; the resolve_entities default changed from True to internal in lxml 5.0. Explicitly set the security options and review wrappers or custom defaults.
No. Its documented defaults forbid entity declarations and external references, but forbid_dtd defaults to False. Pass forbid_dtd=True when the accepted document format does not need DTDs.
No. Bound compressed and decompressed input sizes, keep parser libraries updated, avoid huge_tree, and do not process XInclude from untrusted trees with an unrestricted loader.