Introduction
An oversight in the lxml Python library's default configuration left two commonly used XML parser entry points open to XML External Entity (XXE) injection. Any attacker who can supply XML input can silently read local files from the server. lxml is one of the most depended-upon libraries in the Python ecosystem, with over 316 million downloads in the past month and more than 13.4 million daily downloads on PyPI. That makes the blast radius substantial for any organization running Python services that parse XML.
Technical Information
Root Cause: Inconsistent Default Hardening Across Parser Subclasses
The vulnerability traces back to an incomplete security hardening effort. lxml 5.0 changed the resolve_entities default for the main XMLParser() and HTMLParser classes from True to 'internal', which tells the underlying libxml2 engine to expand only entities defined inline in the document's own DTD. Two other parser entry points were not updated at the same time:
etree.iterparse()etree.ETCompatXMLParser()
Both kept resolve_entities=True as their default parameter value in every lxml version before 6.1.0, so any application using either entry point with default settings was silently exposed to XXE.
The table below shows the default behavior before and after the fix:
| Parser Entry Point | Default Before 6.1.0 | Default After 6.1.0 | Risk if resolve_entities=True |
|---|---|---|---|
etree.iterparse() | True | internal | Local file disclosure |
etree.ETCompatXMLParser() | True | internal | Local file disclosure |
XMLParser() | internal (since 5.0) | internal | Safer default by design |
How the Attack Works
When resolve_entities=True, libxml2 resolves all entity declarations during parsing, including external entities that use a SYSTEM or PUBLIC identifier pointing to a URI. An attacker who can supply XML input to an application using one of the vulnerable parsers can craft a payload that defines an external entity referencing a local file through a file:// URI. The parser expands the entity and embeds the target file's contents in the parsed XML output.
The attack is straightforward:
- The attacker identifies an application endpoint that accepts XML input and parses it using
etree.iterparse()oretree.ETCompatXMLParser()with default settings. - The attacker submits XML containing a DOCTYPE declaration with an external entity definition, such as
<!ENTITY e SYSTEM "file:///etc/hostname">, and references that entity in the document body. - The parser resolves the external entity, reads the specified local file, and substitutes its contents into the parsed element text.
- The application returns or processes the parsed data, which now contains the file contents. That completes the exfiltration.
The CVSS 3.1 vector string AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N describes the attack: network exploitable, low complexity, no privileges or user interaction required, high confidentiality impact, and no integrity or availability impact.
The no_network Trap
Setting no_network=True on the parser does not mitigate this vulnerability. The flag stops the parser from making network requests, which blocks http:// and https:// entity URIs, but it does not stop resolution of local file:// URIs or equivalent local path references. This is a common misconfiguration, and it can give defenders a false sense of security.
Impact Considerations
Which files an attacker can read depends on the runtime environment. The parsing process's effective user privileges determine filesystem access, and container boundaries, chroot jails, and mandatory access controls such as SELinux or AppArmor profiles can narrow what is readable. In many deployments, though, the parsing process has enough privileges to read sensitive configuration files, credentials, or application source code.
Patch Information
lxml 6.1.0 patches the vulnerability in commit ab431ea0b9a7357d968f1d1c5c614649e9aaf358, authored by Stefan Behnel (scoder) on April 10, 2026, and committed on April 12, 2026. The commit message states the intent directly: "Set resolve_entities='internal' as default for all parser subclasses."
The patch is minimal by design. It touches two Cython source files, with 9 additions and 7 deletions, and brings the previously overlooked iterparse() and ETCompatXMLParser() code paths in line with the security posture the main XMLParser adopted in lxml 5.0.
In src/lxml/iterparse.pxi, the iterparse class had resolve_entities=True in both its docstring signature and its __init__() parameter list. The patch changes both:
- compact=True, resolve_entities=True, remove_comments=False, + compact=True, resolve_entities='internal', remove_comments=False,
It also updates the documentation comment:
- - resolve_entities: replace entities by their text value (default: True) + - resolve_entities: replace entities by their text value + (default: 'internal' only)
In src/lxml/parser.pxi, the patch makes the same default swap in three places: the XMLParser docstring signature, the ETCompatXMLParser docstring signature, and the ETCompatXMLParser.__init__() parameter list:
- remove_blank_text=False, resolve_entities=True, + remove_blank_text=False, resolve_entities='internal',
With resolve_entities='internal', lxml tells libxml2 to expand only entities defined inline in the document's own DTD (internal subset entities). It no longer resolves external entity declarations, the ones with a SYSTEM or PUBLIC identifier pointing to a URI like file:///etc/hostname. That closes the XXE vector for local file reads and still allows ordinary internal entity shortcuts like ©right; defined in the XML document itself.
As the maintainer noted on the Launchpad bug tracker, the fix is a backwards-incompatible change to the default argument. That is why it could not ship as a point release (e.g., 6.0.4) and needed a new minor version instead.
Mitigation Options
For organizations that cannot upgrade immediately, these are the available controls:
| Mitigation Option | Stops Local File Reads | Deployment Effort | Notes |
|---|---|---|---|
| Upgrade to lxml 6.1.0 | Yes | Medium | Fixes defaults and standardizes behavior across all parsers |
Set resolve_entities='internal' | Yes | Low | Vendor-recommended workaround that restricts resolution to internal definitions |
Set resolve_entities=False | Yes | Low | Completely disables entity resolution |
Set no_network=True | No | Low | Ineffective against local file disclosure via file:// URIs |
Development teams should audit their codebases for uses of iterparse and ETCompatXMLParser, and configure CI/CD pipelines to flag pull requests that explicitly set resolve_entities=True in code that handles untrusted XML input.
Affected Systems and Versions
Every lxml version before 6.1.0 is affected when an application uses etree.iterparse() or etree.ETCompatXMLParser() with the default resolve_entities=True configuration.
- Affected versions: lxml < 6.1.0
- Patched version: lxml 6.1.0 (released April 18, 2026)
- Vulnerable configurations: Any application that parses untrusted XML using
etree.iterparse()oretree.ETCompatXMLParser()without explicitly settingresolve_entities='internal'orresolve_entities=False - Not directly affected: Applications using only
XMLParser()or HTML parsers, which were hardened in lxml 5.0
lxml is a transitive dependency of many Python packages, so organizations should check their dependency chains for indirect inclusion as well as their code for direct usage.
Vendor Security History
The lxml project has a track record of fixing complex parsing and sanitization edge cases. Recent advisories give useful context on adjacent attack surfaces:
| Component | Issue Summary | Severity | Date Published |
|---|---|---|---|
| lxml core | Default configuration allows XXE to local files (CVE-2026-41066) | High (7.5) | April 18, 2026 |
| lxml_html_clean | <base> tag injection hijacks relative URLs (CVE-2026-28350) | Moderate (6.1) | March 2, 2026 |
| lxml HTML Cleaner | Crafted SVG embedded scripts pass through sanitizer | Moderate | December 12, 2021 |
The project's formal security policy asks for private vulnerability disclosure and sets a 90-day remediation window. The quick turnaround on CVE-2026-41066 shows responsive maintenance. Still, the recurring sanitization bypasses suggest that organizations using lxml for security-critical parsing or cleaning should keep defense in depth in place rather than relying solely on the library's defaults.
References
- NVD: CVE-2026-41066
- CVE Record: CVE-2026-41066
- GitHub Security Advisory: GHSA-vfmq-68hx-4jfw (lxml/lxml)
- GitHub Advisory Database: GHSA-vfmq-68hx-4jfw
- Launchpad Bug Report #2146291
- Patch Commit: ab431ea0b9a7357d968f1d1c5c614649e9aaf358
- Patch Commit API
- lxml on PyPI
- lxml Download Statistics
- Tenable: CVE-2026-41066
- lxml API Documentation: iterparse
- lxml Security Overview
- Related Advisory: lxml_html_clean base tag injection (GHSA-xvp8-3mhv-424c)



