Introduction
The scripts/prepare-overview-review.py file handles unpacking and inspecting document archives — likely .pptx, .docx, or similar OOXML-based zip files — using ZipFile and BadZipFile from Python's zipfile module. To parse the XML content extracted from those archives, the script imported xml.dom.minidom and xml.parsers.expat.ExpatError at line 26-27. That single import is the root of the issue: Python's standard library XML parsers, including minidom, are not safe by default when handling untrusted or externally-sourced XML.
This matters because any script that unzips a file and parses its internal XML — a very common pattern for handling Office documents, SVGs, RSS feeds, or SOAP payloads — inherits this risk automatically, often without the developer realizing it. If prepare-overview-review.py is ever run against a file that wasn't fully trusted (e.g., an uploaded document, a file fetched from a third party, or a report generated by another tool in the pipeline), the underlying XML parser will happily resolve external entities embedded in that file.
The Vulnerability Explained
Before the fix, the relevant imports looked like this:
import math
from pathlib import Path
import posixpath
from xml.dom import minidom
from xml.parsers.expat import ExpatError
from zipfile import BadZipFile, ZipFile
xml.dom.minidom is built on top of xml.parsers.expat, and by default it does not disable DTD processing or external entity resolution. This means that if the script calls something like minidom.parseString(xml_data) or minidom.parse(file_handle) on XML content extracted from a zip archive, an attacker who controls that archive can embed a malicious DOCTYPE declaration such as:
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
When minidom parses this, it will attempt to resolve the xxe entity by reading /etc/passwd (or any other file the process can access) and inline its contents into the parsed document. Depending on how the script surfaces or logs parsed XML data — for instance, if it prints element text or writes it to a review report — the contents of that file could leak into output that a reviewer or downstream system sees.
Beyond local file disclosure, the same primitive can be used for:
- SSRF: pointing the SYSTEM identifier at an internal URL (http://169.254.169.254/latest/meta-data/) to probe internal network services.
- Denial of Service: the classic "billion laughs" entity-expansion attack, where nested entity definitions cause exponential memory consumption when expanded.
In the context of prepare-overview-review.py, the attack scenario is straightforward: since the script's job is to open a zip archive and inspect XML parts inside it, an attacker only needs to craft one malicious .zip/.pptx/.docx-style file containing a poisoned XML entry. If that file is fed into the review pipeline — whether through an upload flow, a shared drive, or an automated ingestion job — the vulnerable parser processes the payload with no warning.
The Fix
The PR makes a minimal, surgical change to the imports at the top of the file — no logic elsewhere in the script needed to change, because defusedxml.minidom is designed as an API-compatible drop-in replacement for xml.dom.minidom.
Before:
from xml.dom import minidom
from xml.parsers.expat import ExpatError
After:
from defusedxml import minidom
from pyexpat import ExpatError
Here's why each half of the change matters:
from defusedxml import minidomreplaces the vulnerable native parser withdefusedxml's hardened version.defusedxml.minidomdisables DTD processing, external entity resolution, and external general/parameter entities by default, and raises a clear exception (EntitiesForbidden,DTDForbidden, etc.) if malicious constructs are detected in the input — instead of silently resolving them.from pyexpat import ExpatErrorkeeps exception handling working correctly.ExpatErroris the exception type raised on malformed XML, and sincedefusedxmlwraps the same underlyingpyexpatC extension, importingExpatErrordirectly frompyexpat(rather thanxml.parsers.expat) keeps the error-handling code (except ExpatError:) functioning exactly as before, with no other code changes required.
Because defusedxml.minidom mirrors the public API of xml.dom.minidom (parse(), parseString(), Node, etc.), the rest of prepare-overview-review.py — wherever it calls minidom.parse(...) on extracted archive contents — continues to work unchanged for well-formed, benign XML. The only behavioral difference is that malicious XML payloads now fail safely with a defusedxml-specific exception instead of being silently parsed and potentially exploited.
Key Takeaways
- The vulnerable pattern was a single import line:
from xml.dom import minidominscripts/prepare-overview-review.py:26, feeding XML extracted from zip/document archives into an XXE-capable parser. prepare-overview-review.pyprocesses zip-based document archives (viaZipFile/BadZipFile), making it a realistic target for a maliciously crafted archive containing an XXE payload.- The fix required no changes to parsing logic — swapping
xml.dom.minidom→defusedxml.minidomandxml.parsers.expat.ExpatError→pyexpat.ExpatErrorwas sufficient becausedefusedxmlis API-compatible. - This was flagged as a "defensive hardening" fix rather than a confirmed active exploit — but XXE primitives left in place are exactly the kind of pattern automated exploit tooling looks to chain with other weaknesses.
- Static analysis rules covering Bandit's B313–B320 and B405–B410 checks exist precisely to catch native XML library imports like this before they ship.
How Orbis AppSec Detected This
- Source: XML content embedded inside zip archives (e.g.,
.pptx/.docx-style files) read viaZipFileinscripts/prepare-overview-review.py. - Sink:
xml.dom.minidomparsing calls (line 28 area), which resolve DTDs and external entities by default. - Missing control: No use of a hardened, entity-resolution-disabled XML parser — the native
xml.dom.minidommodule was used as-is on data derived from an archive that could originate from an untrusted source. - CWE: CWE-611 — Improper Restriction of XML External Entity Reference.
- Fix: Replaced
xml.dom.minidomandxml.parsers.expat.ExpatErrorwithdefusedxml.minidomandpyexpat.ExpatError, disabling external entity and DTD 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
XXE vulnerabilities are easy to introduce and easy to overlook, because the vulnerable code often looks completely ordinary — a plain import xml.dom.minidom and a call to parse(). In scripts/prepare-overview-review.py, that pattern sat quietly in a script designed to unpack and inspect archive-based documents, a workflow that regularly touches XML data from sources outside the developer's direct control. The fix was small — two import lines — but it closes off a real class of attacks: local file disclosure, SSRF, and XML-based denial of service. Whenever your Python code parses XML, especially XML pulled from files, uploads, or archives, default to defusedxml rather than the standard library's XML modules, and let static analysis tools catch the cases you miss.