How XML External Entity (XXE) Injection Happens in Python and How to Fix It
Introduction
In the scripts/screenshots/ui.py file, a critical XML External Entity (XXE) vulnerability was discovered at line 9 where the code imported Python's native xml.etree.ElementTree library:
import xml.etree.ElementTree as ET
This seemingly innocent import created a significant security risk. The native Python XML libraries enable external entity processing by default, which means any XML parsed by this code could be weaponized by an attacker to read sensitive files, perform server-side request forgery (SSRF) attacks, or trigger denial-of-service conditions through XML bombs.
The vulnerability matters because the UI screenshot automation tool likely processes XML configuration files or responses from external services. If an attacker could inject malicious XML into these inputs, they could potentially read the system's SSH keys, database credentials, or other sensitive files stored on the same server where the screenshot tool runs.
The Vulnerability Explained
XML External Entity (XXE) injection is a server-side vulnerability that exploits how XML parsers handle external entity definitions. Here's how it works:
The Vulnerable Code Pattern:
When Python's xml.etree.ElementTree parses XML without explicit security configuration, it honors external entity declarations like this:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<data>&xxe;</data>
</root>
If the screenshot tool parsed this malicious XML, the parser would attempt to read /etc/passwd and include its contents in the parsed document. An attacker could then exfiltrate that data through error messages or response content.
The Specific Risk in ui.py:
The scripts/screenshots/ui.py file uses the vulnerable import at line 9:
import xml.etree.ElementTree as ET
Any subsequent calls to ET.parse() or ET.fromstring() in this file would be vulnerable. For example:
tree = ET.parse('config.xml') # Vulnerable to XXE
root = tree.getroot()
Attack Scenarios:
-
File Disclosure: An attacker could craft a malicious XML configuration file that, when parsed, reads
/etc/passwdor application secrets from environment variables written to XML files. -
SSRF (Server-Side Request Forgery): Using XXE to make the server connect to internal services:
xml <!ENTITY xxe SYSTEM "http://localhost:8080/admin"> -
Denial-of-Service (XML Bomb): The "billion laughs" attack uses entity expansion to consume server memory:
```xml
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
&lol3;
```
Why This Matters for This Code:
The screenshot automation tool likely processes XML from configuration files or external APIs. If any of these sources are attacker-controlled or can be manipulated through a supply chain attack, the XXE vulnerability becomes exploitable. The tool runs with the permissions of the user or service account that executes it, so any files readable by that account are at risk.
The Fix
The fix is simple but critical: replace Python's native XML library with defusedxml, which is hardened against XXE attacks.
Before (Vulnerable):
import xml.etree.ElementTree as ET
After (Secure):
import defusedxml.ElementTree as ET
This single-line change completely eliminates the XXE vulnerability because defusedxml disables external entity processing, DTD processing, and other dangerous XML features by default.
How This Solves the Problem:
The defusedxml library is a drop-in replacement for Python's native XML libraries that:
- Disables external entity processing – The parser no longer honors
<!ENTITY>declarations that reference external files or URLs - Prevents billion laughs attacks – Entity expansion is limited or disabled
- Maintains API compatibility – Code using
ET.parse(),ET.fromstring(), etc. works identically without modification - Provides sensible defaults – Security is enabled automatically; developers don't need to remember to configure parser options
When defusedxml.ElementTree encounters the malicious XXE payload shown earlier, it silently ignores the external entity definition and parses only the safe XML structure. The attacker's attempt to read /etc/passwd fails silently, preventing file disclosure.
Prevention & Best Practices
For Python Developers:
- Always use
defusedxmlinstead of native XML libraries – Make it a project standard:
```python
# Good
import defusedxml.ElementTree as ET
# Bad
import xml.etree.ElementTree as ET
```
-
Add
defusedxmlto your project dependencies:
bash pip install defusedxml -
Audit existing code for vulnerable XML imports – Search your codebase for:
-import xml.etree.ElementTree
-import xml.dom.minidom
-import xml.sax
-import xmlrpc
All of these should be replaced with their defusedxml equivalents.
-
Use static analysis tools – Configure Semgrep, Bandit, or your SAST scanner to flag native XML library usage and recommend
defusedxml. -
Never disable XXE protections – Even if you think you need to process external entities, use
defusedxmlwith explicit configuration rather than reverting to unsafe libraries.
Security Standards:
- CWE-611: Improper Restriction of XML External Entity Reference
- OWASP A05:2021: Security Misconfiguration (XXE is often categorized here)
- OWASP A03:2021: Injection (XXE is a form of injection attack)
Key Takeaways
-
Native Python XML libraries are unsafe by default –
xml.etree.ElementTree,xml.dom.minidom, andxml.saxall enable external entity processing, creating XXE vulnerabilities. -
defusedxml is a drop-in replacement – You can change a single import statement and gain complete XXE protection without rewriting any other code.
-
XXE affects more than file disclosure – This vulnerability can be chained with SSRF attacks to access internal services, or weaponized as a denial-of-service vector through XML bombs.
-
Static analysis caught this proactively – Semgrep flagged the vulnerable import in
scripts/screenshots/ui.py:9before it could be exploited, demonstrating the value of automated security scanning. -
Defense in depth matters – Even though the screenshot tool may not directly expose XML parsing to untrusted users, replacing the native library removes an exploit primitive that could be chained with other vulnerabilities.
How Orbis AppSec Detected This
Source: XML configuration files or API responses parsed by the screenshot automation tool in scripts/screenshots/ui.py
Sink: The xml.etree.ElementTree import at line 9, which is used for all subsequent XML parsing operations in the file via ET.parse() and ET.fromstring() calls
Missing control: No protection against external entity processing; the native Python XML library was used without disabling XXE-vulnerable features
CWE: CWE-611 – Improper Restriction of XML External Entity Reference
Fix: Replaced import xml.etree.ElementTree as ET with import defusedxml.ElementTree as ET to disable external entity processing by default
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
XML External Entity (XXE) injection is a powerful attack vector that Python developers often overlook because the vulnerability is hidden in library defaults rather than in application code. The fix is straightforward: replace native XML libraries with defusedxml.
This vulnerability in scripts/screenshots/ui.py demonstrates why proactive security hardening matters. Even if the screenshot tool isn't directly exposed to untrusted XML input today, removing this exploit primitive raises the bar against increasingly sophisticated automated attack tools. By switching to defusedxml with a single-line change, the development team eliminated an entire class of vulnerabilities without sacrificing functionality or performance.
Make defusedxml a standard practice in your Python projects. It's a small change with massive security benefits.