FlawOpen

flawopen.com/XML External Entities (XXE)/Python XML parser security

Is Python xml.etree.ElementTree safe from XXE?

CWE-611: XML External Entity ReferenceReference page
Short answer

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.
Step 1

Untrusted XML reaches a parser

An upload, SVG, SAML message, or integration payload crosses a trust boundary and is passed to an XML API.

Step 2

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.

Step 3

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.

Step 4

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.

RISKY — parser version and options change the boundary
# 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)
HARDENED — reject DTDs and external resources
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.

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

Does ElementTree read local files through external entities by default?

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.

Is current lxml unsafe by default?

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.

Does defusedxml reject every DTD by default?

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.

Does a safe parser remove every XML resource risk?

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.

Primary references

See also XML External Entity (XXE) Injection (CWE-611): How It Works and How to Prevent It — flawopen.com Is .NET XmlReader safe from XXE? Java XXE: Is DocumentBuilderFactory Secure by Default?