Automated Threat Modeling

Brief Summary: CVE-2026-41066 — lxml XXE Vulnerability Enables Local File Disclosure via Default Parser Configuration

A short review of CVE-2026-41066, a high-severity XXE vulnerability in the lxml Python library where default parser configurations allow local file reads, with patch analysis and mitigation guidance.

CVE Analysis

8 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-04-24

Brief Summary: CVE-2026-41066 — lxml XXE Vulnerability Enables Local File Disclosure via Default Parser Configuration
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

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 PointDefault Before 6.1.0Default After 6.1.0Risk if resolve_entities=True
etree.iterparse()TrueinternalLocal file disclosure
etree.ETCompatXMLParser()TrueinternalLocal file disclosure
XMLParser()internal (since 5.0)internalSafer 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:

  1. The attacker identifies an application endpoint that accepts XML input and parses it using etree.iterparse() or etree.ETCompatXMLParser() with default settings.
  2. 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.
  3. The parser resolves the external entity, reads the specified local file, and substitutes its contents into the parsed element text.
  4. 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 &copyright; 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 OptionStops Local File ReadsDeployment EffortNotes
Upgrade to lxml 6.1.0YesMediumFixes defaults and standardizes behavior across all parsers
Set resolve_entities='internal'YesLowVendor-recommended workaround that restricts resolution to internal definitions
Set resolve_entities=FalseYesLowCompletely disables entity resolution
Set no_network=TrueNoLowIneffective 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() or etree.ETCompatXMLParser() without explicitly setting resolve_entities='internal' or resolve_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:

ComponentIssue SummarySeverityDate Published
lxml coreDefault 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 CleanerCrafted SVG embedded scripts pass through sanitizerModerateDecember 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

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization