Back to Blog
medium SEVERITY7 min read

How XML External Entity (XXE) Injection happens in Python and how to fix it

A medium-severity XML External Entity (XXE) vulnerability was discovered in `listKeyboardLayouts.py`, where Python's native `xml.etree.ElementTree` library was used to parse XML data. This library is susceptible to XXE attacks, which can allow attackers to read local files, perform server-side request forgery, or cause denial of service. The fix replaces the unsafe import with `defusedxml.ElementTree`, a drop-in hardened alternative recommended by the Python documentation itself.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is an XML External Entity (XXE) injection vulnerability (CWE-611) in Python, found in `listKeyboardLayouts.py`. Python's built-in `xml.etree.ElementTree` does not disable external entity processing by default, allowing attackers who control XML input to exfiltrate local files or trigger SSRF. The fix is a one-line change: replace `import xml.etree.ElementTree as et` with `import defusedxml.ElementTree as et`, which disables all dangerous XML features while preserving normal parsing behavior.

Vulnerability at a Glance

cweCWE-611
fixReplace `xml.etree.ElementTree` with `defusedxml.ElementTree` (drop-in hardened replacement)
riskAttackers can read local files, trigger SSRF, or cause DoS by embedding malicious XML entities
languagePython
root causePython's native `xml.etree.ElementTree` processes external XML entities by default
vulnerabilityXML External Entity (XXE) Injection

How XML External Entity (XXE) Injection Happens in Python and How to Fix It

The Vulnerability at a Glance

Field Detail
Vulnerability XML External Entity (XXE) Injection
CWE CWE-611
Language Python
Risk File disclosure, SSRF, DoS via malicious XML input
Root Cause xml.etree.ElementTree processes external entities by default
Fix Drop-in replacement with defusedxml.ElementTree

Introduction

The listKeyboardLayouts.py file does exactly what its name suggests — it reads and parses XML data to enumerate available keyboard layouts. Straightforward enough. But the way it imported Python's XML library introduced a subtle yet serious security flaw: it used the native xml.etree.ElementTree, a parser that has no protection against XML External Entity (XXE) attacks.

This is the kind of vulnerability that doesn't announce itself loudly. The code looks clean, the functionality works, and nothing seems obviously wrong — until someone hands it a crafted XML file.


The Vulnerability Explained

At the top of listKeyboardLayouts.py, a single import line determined how all XML parsing would behave throughout the file:

# Before the fix — vulnerable
import xml.etree.ElementTree as et

This import is then used in the read_names() function to parse XML from a file path:

def read_names(path: str) -> list[str]:
    names = []
    ...

Python's xml.etree.ElementTree — along with the other flagged native XML modules (xml.sax, xml.dom.minidom, xml.dom.pulldom, xml.etree.cElementTree) — does not disable external entity processing by default. This means if an attacker can influence the content of the XML file being parsed (or the path itself), they can craft a payload like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE layouts [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<layouts>
  <layout name="&xxe;" />
</layouts>

When et.parse(path) processes this file, the parser dutifully resolves &xxe; by reading /etc/passwd on the server and substituting its contents into the XML tree. If the application then returns or logs the name attribute, the attacker has successfully exfiltrated a sensitive system file.

What's the Real-World Impact?

For listKeyboardLayouts.py specifically, the attack surface depends on where the XML files come from. If the path argument to read_names() is derived from user input, a network share, a third-party package, or any source that could be tampered with, an attacker could:

  • Read local files: /etc/passwd, /etc/shadow, application config files with database credentials, private keys
  • Trigger Server-Side Request Forgery (SSRF): Replace file:// with http:// to make the server issue outbound HTTP requests to internal services (e.g., cloud metadata endpoints like http://169.254.169.254/latest/meta-data/)
  • Cause Denial of Service: Use a "Billion Laughs" entity expansion attack to exhaust memory

Even if the current call sites for read_names() appear safe today, the presence of a vulnerable parser is an exploit primitive — a building block that automated attack tooling or a future code change could leverage.


The Fix

The fix is elegantly minimal: a single-line import change.

- import xml.etree.ElementTree as et
+ import defusedxml.ElementTree as et

defusedxml is a Python library created specifically to address the insecurity of Python's native XML parsers. It provides drop-in replacements for all the standard XML modules and disables the following dangerous features by default:

  • External entity expansion (the core XXE vector)
  • DTD processing (prevents Billion Laughs / entity expansion DoS)
  • External DTD loading
  • Python entity expansion

Because defusedxml.ElementTree exposes the same API as xml.etree.ElementTree, no other code in the file needed to change. The et.parse(), et.fromstring(), and all other calls work identically — they just now raise a DefusedXmlException if a malicious payload is encountered, rather than silently processing it.

Before vs. After

Before (vulnerable):

import xml.etree.ElementTree as et

def read_names(path: str) -> list[str]:
    names = []
    tree = et.parse(path)  # Processes external entities — dangerous!
    ...

After (hardened):

import defusedxml.ElementTree as et

def read_names(path: str) -> list[str]:
    names = []
    tree = et.parse(path)  # External entities blocked — safe!
    ...

The security improvement is profound despite the change being cosmetic. Any XML file that attempts to define or resolve external entities will now raise an exception rather than being silently exploited.


Prevention & Best Practices

1. Always Use defusedxml for XML Parsing in Python

The Python documentation itself warns against using the native XML libraries for untrusted input and recommends defusedxml. Install it with:

pip install defusedxml

Then replace your imports:

Vulnerable Import Safe Replacement
import xml.etree.ElementTree import defusedxml.ElementTree
import xml.sax import defusedxml.sax
import xml.dom.minidom import defusedxml.minidom
import xml.dom.pulldom import defusedxml.pulldom
from xml.etree.cElementTree import ... from defusedxml.cElementTree import ...

2. Validate File Paths Before Parsing

Even with a safe parser, validate the path argument to read_names() to ensure it points to an expected location:

import os

def read_names(path: str) -> list[str]:
    # Ensure path is within the expected directory
    base_dir = "/usr/share/keyboard-layouts"
    resolved = os.path.realpath(path)
    if not resolved.startswith(base_dir):
        raise ValueError(f"Unexpected path: {path}")
    ...

3. Run Static Analysis in CI/CD

The Bandit rules that flagged this issue (B313–B320, B405–B410) are available in both Bandit and Semgrep. Add them to your CI pipeline:

# Example GitHub Actions step
- name: Run Semgrep
  run: semgrep --config=p/python --config=p/bandit .

4. Principle of Least Privilege for File Access

Ensure the process running listKeyboardLayouts.py only has read access to the directories it legitimately needs. This limits the blast radius if an XXE attack does succeed.

Security Standards Reference


Key Takeaways

  • xml.etree.ElementTree is not safe for parsing untrusted XML — it's a well-known issue documented in the Python standard library docs, yet it remains a common mistake because the import looks harmless.
  • The fix in listKeyboardLayouts.py was a single import swapdefusedxml is a true drop-in replacement, so there's no excuse not to use it.
  • The read_names(path: str) function now safely handles malicious XML files without any logic changes to the parsing code itself.
  • Bandit rules B405–B410 specifically target XML library imports — if these rules fire in your codebase, treat them as high priority, not noise.
  • XXE is an exploit primitive: even if today's call sites seem safe, leaving a vulnerable parser in place is a bet that no future code change or input source will ever be attacker-influenced.

How Orbis AppSec Detected This

  • Source: The path parameter passed to read_names(path: str) in listKeyboardLayouts.py, which accepts a file path to an XML document
  • Sink: et.parse(path) — the call to xml.etree.ElementTree.parse() at the entry point of XML processing, where external entity resolution occurs
  • Missing control: No protection against external entity expansion; Python's native xml.etree.ElementTree has no mechanism to disable XXE by default
  • CWE: CWE-611 — Improper Restriction of XML External Entity Reference
  • Fix: Replaced import xml.etree.ElementTree as et with import defusedxml.ElementTree as et on line 1 of listKeyboardLayouts.py

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

A single import statement was the difference between a parser that silently reads your /etc/passwd and one that safely rejects malicious XML. The vulnerability in listKeyboardLayouts.py is a textbook example of why "it works" is not the same as "it's secure" — Python's xml.etree.ElementTree parses XML perfectly well, it just does so without any of the guardrails that modern security requires.

The fix — swapping to defusedxml.ElementTree — took one line of code and zero changes to application logic. That's about as good a security-to-effort ratio as you'll ever find. The lesson for Python developers is clear: whenever you're parsing XML, reach for defusedxml first. Make it a habit, add Bandit or Semgrep to your CI pipeline to catch it when you forget, and treat any use of native XML libraries in code that handles external input as a bug waiting to be exploited.


References

Frequently Asked Questions

What is XML External Entity (XXE) injection?

XXE is an attack where malicious XML input includes references to external entities, tricking the XML parser into reading local files, making network requests, or causing denial of service.

How do you prevent XXE injection in Python?

Replace Python's native XML libraries (like `xml.etree.ElementTree`) with `defusedxml`, which disables external entity processing and other dangerous XML features by default.

What CWE is XML External Entity injection?

XXE injection is classified as CWE-611: Improper Restriction of XML External Entity Reference.

Is input validation enough to prevent XXE in Python?

No. Input validation alone is insufficient because the vulnerability lies in the parser itself processing external entities before your validation logic runs. Using a safe parser like `defusedxml` is the correct fix.

Can static analysis detect XXE vulnerabilities in Python?

Yes. Tools like Semgrep and Bandit (rules B313–B320, B405–B410) can detect use of Python's native XML libraries and flag them as potential XXE risks, as happened in this case.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #166

Related Articles

high

How HTTP Transport Hijacking via Prototype Pollution happens in JavaScript and how to fix it

CVE-2026-42033 is a high-severity prototype pollution vulnerability in axios that allows attackers to hijack the HTTP transport layer used by the library. The deltamod project was running axios 1.14.0, which lacked the hardened transport configuration introduced in 1.18.0 — including an explicit `https-proxy-agent` dependency and an upgraded `follow-redirects` floor. Upgrading to axios 1.18.0 closes the attack surface by ensuring that object prototype manipulation cannot silently redirect or int

high

How Arbitrary Code Execution via Template Imports happens in JavaScript and how to fix it

CVE-2026-4800 is a high-severity arbitrary code execution vulnerability in lodash-es versions prior to 4.18.0, triggered through untrusted input passed to lodash's template engine. The fix upgrades lodash-es from 4.17.23 to 4.18.1 using a pnpm override, ensuring all transitive dependents pick up the patched version. This is a concrete reminder that even utility libraries like lodash can become critical attack surfaces when they process user-controlled input.

high

How EL Injection happens in Java JSF applications and how to fix it

A high-severity Expression Language (EL) injection vulnerability was discovered and fixed in `PrimeFacesResourceProcessor.java`, a JSF phase listener responsible for resolving the PrimeFaces theme configuration. The flaw allowed a dynamically sourced theme parameter value to be passed directly into an EL expression factory without first verifying whether the value was actually an EL expression or plain text. The fix introduces explicit input branching that separates EL expressions from literal s

high

How Prototype Pollution happens in Node.js async libraries and how to fix it

A high-severity prototype pollution vulnerability (CVE-2021-43138) was discovered in the `async` npm package versions prior to 3.2.2, affecting the `node-red-contrib-opcua` project. By exploiting crafted input passed through async's utility functions, an attacker could corrupt JavaScript's `Object.prototype`, potentially enabling privilege escalation or remote code execution. Upgrading `async` from `3.2.1` to `^3.2.2` in both `package.json` and `package-lock.json` eliminates the attack surface e

high

How SQL Injection happens in Python BigQuery connectors and how to fix it

A high-severity SQL injection vulnerability was discovered in a BigQuery connector's query-building logic, where Python f-strings interpolated user-controlled identifiers—project_id, dataset_id, table_id, and timestamp_column—directly into SQL without validation. An attacker with control over connector configuration could inject arbitrary BigQuery SQL, including destructive statements. The fix introduces strict allowlist-based identifier validation using compiled regular expressions before any S

medium

How Denial of Service via Catastrophic Backtracking happens in Node.js and how to fix it

CVE-2026-4867 is a Regular Expression Denial of Service (ReDoS) vulnerability in the `path-to-regexp` package (versions prior to 0.1.13) that allows an attacker to craft malformed URL parameters that cause catastrophic backtracking in the regex engine, effectively hanging the Node.js event loop. The fix upgrades `path-to-regexp` from 0.1.12 to 0.1.13 and pins the version via an `overrides` field in `package.json` to ensure the patched version is used throughout the entire dependency tree. Any Ex