The Exploit
An attacker with the ability to supply a malicious OOXML file (e.g., a .docx document) to Apache Tika for parsing can inject an XML External Entity (XXE) payload into the docProps/custom.xml component.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="test">
<vt:lpwstr>&xxe;</vt:lpwstr>
</property>
</Properties>
When Tika's MetadataExtractor.CustomPropertiesHandler parses this XML via the undefended SAXParser, the entity resolves to the contents of /etc/passwd, which is either written to the document's extracted metadata or disclosed in error messages. The attacker observes the file contents leaking into the parsed metadata output or error logs.
What the Patch Did
Before
private static Metadata parseCustomProperties(String xml) throws Exception {
MetadataExtractor.CustomPropertiesHandler handler =
new MetadataExtractor.CustomPropertiesHandler();
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
SAXParser parser = factory.newSAXParser();
parser.parse(new InputSource(new ByteArrayInputStream(
xml.getBytes(StandardCharsets.UTF_8))), handler);
After
private static Metadata parseCustomProperties(String xml) throws Exception {
MetadataExtractor.CustomPropertiesHandler handler =
new MetadataExtractor.CustomPropertiesHandler();
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
SAXParser parser = factory.newSAXParser();
parser.parse(new InputSource(new ByteArrayInputStream(
xml.getBytes(StandardCharsets.UTF_8))), handler);
The patch adds four SAXParserFactory.setFeature() calls that collectively disable external entity resolution in the parser. The controls are:
http://apache.org/xml/features/disallow-doctype-decl— rejects any DOCTYPE declaration entirely.http://xml.org/sax/features/external-general-entities— disables external general entity resolution (the primary XXE vector).http://xml.org/sax/features/external-parameter-entities— disables parameter entity injection.http://apache.org/xml/features/nonvalidating/load-external-dtd— prevents loading external DTD subsets.
Root Cause
CWE-611: Improper Restriction of XML External Entity Reference ("XXE Injection").
The attacker controls the contents of the OOXML file's docProps/custom.xml entry, which is extracted and passed as the xml parameter to parseCustomProperties(). The SAXParserFactory is instantiated with no XXE defences; by default, both general and parameter entities are resolved. When parser.parse() is called on the untrusted XML stream, the SAX parser follows entity declarations and fetches resources from the filesystem or network on behalf of the application. The trust boundary is crossed when user-supplied OOXML enters the parsing pipeline without entity resolution guards.
Why It Works
The load-bearing line is:
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Removing it alone would still leave the parser vulnerable: an attacker could craft a well-formed XML document without a DOCTYPE but leverage parameter entity tricks or namespace-based attacks. However, this line is the highest-impact single control because it rejects the attack at the grammar level — no DOCTYPE means no entity declarations can be introduced.
The engineer added the three remaining features (external-general-entities, external-parameter-entities, load-external-dtd) as defence in depth. Together, they form a layered barrier:
- If DOCTYPE somehow gets through, general entity resolution is already off.
- If an attacker crafts a parameter entity attack, that vector is blocked separately.
- If a legacy parser version has a bypass, the external DTD loader is still disabled.
Each feature targets a different XXE attack surface; removing any one reduces the margin of safety. The patch as written ensures no single feature regression will re-open the vulnerability.
Hardening Checklist
- Disable DOCTYPE declarations in all
SAXParserFactoryorXMLInputFactoryinstances handling untrusted XML by settinghttp://apache.org/xml/features/disallow-doctype-decltotrue. - Disable external entity resolution by setting both
http://xml.org/sax/features/external-general-entitiesandhttp://xml.org/sax/features/external-parameter-entitiestofalseon every parser factory before parsing. - Audit all XML parsing code (OOXML, ODP, SVG, or any embedded XML) — use the OWASP XXE Prevention Cheat Sheet to verify all sinks are protected, including DOM parsers (
DocumentBuilderFactory), StAX (XMLInputFactory), and XPath evaluators. - Use an allowlist approach: if the XML format is known, disable schema validation and only enable the specific XML features actually needed (namespace awareness, etc.); disable everything else by default.
- Add XXE-payload test cases to your test suite — craft OOXML files with XXE payloads in each structural component (core properties, custom properties, document relationships) and verify they do not leak file contents or cause out-of-band requests.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-66755
- OWASP XXE Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- Apache Xerces2 SAX Parser Feature Documentation