This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 4e9d4268fd Issue #8163 : Harden XmlInputStream StAX factory against 
XXE (#8164)
4e9d4268fd is described below

commit 4e9d4268fd07859605e3d3ce51f5785ac82908db
Author: Matt Casters <[email protected]>
AuthorDate: Mon Aug 31 20:27:44 2026 +0200

    Issue #8163 : Harden XmlInputStream StAX factory against XXE (#8164)
    
    Disable DTD processing and external entity resolution on StAX
    XMLInputFactory via XmlParserFactoryProducer, and use that factory
    from XmlInputStream and the remaining raw/partial StAX call sites.
---
 THREAT_MODEL.md                                    |  5 ++-
 .../java/org/apache/hop/core/xml/XmlFormatter.java |  3 +-
 .../hop/core/xml/XmlParserFactoryProducer.java     | 23 +++++++++++
 .../java/org/apache/hop/core/xml/XmlUtilsTest.java | 48 ++++++++++++++++++++++
 .../transforms/excelinput/staxpoi/StaxUtil.java    |  7 +---
 .../transforms/webservices/WebService.java         |  5 +--
 .../xml/advancedxmloutput/AdvancedXmlOutput.java   | 11 ++---
 .../xml/xmlinputstream/XmlInputStream.java         |  4 +-
 .../xml/xmlinputstream/XmlInputStreamTest.java     | 28 +++++++++++++
 9 files changed, 113 insertions(+), 21 deletions(-)

diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 7de2dd274f..bcf7186cd2 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -156,7 +156,10 @@ where strict outbound TLS verification is required (see 
§9).
   
([`XmlParserFactoryProducer.java`](core/src/main/java/org/apache/hop/core/xml/XmlParserFactoryProducer.java))
 with external general/parameter entities and
   external DTD loading disabled and `FEATURE_SECURE_PROCESSING` on — XXE
   file-read/SSRF and entity-expansion are mitigated (verified empirically). The
-  same secure parser is used by the Hop Server remote-add endpoints.
+  same secure parser is used by the Hop Server remote-add endpoints. StAX 
ingest
+  (`XmlInputStream` and other `XMLInputFactory` sites) uses
+  `createSecureXmlInputFactory()`, which disables DTD processing and external
+  entities.
 - **Credential storage — NOT confidential by default.** Connection passwords in
   metadata are by default only **reversibly obfuscated, not encrypted**: the
   built-in `Hop` encoder 
([`HopTwoWayPasswordEncoder.java`](core/src/main/java/org/apache/hop/core/encryption/HopTwoWayPasswordEncoder.java))
 XORs against a
diff --git a/core/src/main/java/org/apache/hop/core/xml/XmlFormatter.java 
b/core/src/main/java/org/apache/hop/core/xml/XmlFormatter.java
index 82915d4101..cd27af78ee 100644
--- a/core/src/main/java/org/apache/hop/core/xml/XmlFormatter.java
+++ b/core/src/main/java/org/apache/hop/core/xml/XmlFormatter.java
@@ -37,7 +37,8 @@ import org.apache.hop.core.exception.HopRuntimeException;
 public class XmlFormatter {
   private static final String TRANSFORM_PREFIX = "  ";
 
-  private static XMLInputFactory INPUT_FACTORY = XMLInputFactory.newInstance();
+  private static XMLInputFactory INPUT_FACTORY =
+      XmlParserFactoryProducer.createSecureXmlInputFactory();
   private static XMLOutputFactory OUTPUT_FACTORY = 
XMLOutputFactory.newInstance();
 
   static {
diff --git 
a/core/src/main/java/org/apache/hop/core/xml/XmlParserFactoryProducer.java 
b/core/src/main/java/org/apache/hop/core/xml/XmlParserFactoryProducer.java
index 07db9f2751..84c3d3ada6 100644
--- a/core/src/main/java/org/apache/hop/core/xml/XmlParserFactoryProducer.java
+++ b/core/src/main/java/org/apache/hop/core/xml/XmlParserFactoryProducer.java
@@ -21,6 +21,7 @@ import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
 import javax.xml.validation.SchemaFactory;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.logging.LogChannel;
@@ -143,4 +144,26 @@ public class XmlParserFactoryProducer {
 
     return factory;
   }
+
+  /**
+   * Creates an instance of {@link XMLInputFactory} with DTD processing and 
external entity
+   * resolution disabled to protect against XML External Entity (XXE) attacks 
and XML entity
+   * expansion bombs.
+   *
+   * <p>{@link XMLConstants#ACCESS_EXTERNAL_DTD} and {@link 
XMLConstants#ACCESS_EXTERNAL_SCHEMA} are
+   * set when the StAX provider recognizes them. Woodstox (the factory on 
Hop's runtime classpath)
+   * does not, so those two calls are best-effort.
+   */
+  public static XMLInputFactory createSecureXmlInputFactory() {
+    XMLInputFactory factory = XMLInputFactory.newInstance();
+    factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+    factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
false);
+    try {
+      factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+      factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+    } catch (IllegalArgumentException e) {
+      // Property not supported by this StAX provider
+    }
+    return factory;
+  }
 }
diff --git a/core/src/test/java/org/apache/hop/core/xml/XmlUtilsTest.java 
b/core/src/test/java/org/apache/hop/core/xml/XmlUtilsTest.java
index d7682559ae..94ed779576 100644
--- a/core/src/test/java/org/apache/hop/core/xml/XmlUtilsTest.java
+++ b/core/src/test/java/org/apache/hop/core/xml/XmlUtilsTest.java
@@ -18,15 +18,21 @@
 package org.apache.hop.core.xml;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
+import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
 import javax.xml.validation.SchemaFactory;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -104,4 +110,46 @@ class XmlUtilsTest {
 
     assertDoesNotThrow(() -> schemaFactory.newSchema(including));
   }
+
+  @Test
+  void secureXmlInputFactoryDisablesDtdAndExternalEntities() {
+    XMLInputFactory factory = 
XmlParserFactoryProducer.createSecureXmlInputFactory();
+
+    assertFalse((Boolean) factory.getProperty(XMLInputFactory.SUPPORT_DTD));
+    assertFalse((Boolean) 
factory.getProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES));
+  }
+
+  @Test
+  void secureXmlInputFactoryDoesNotResolveExternalEntities(@TempDir Path 
tempDir) throws Exception {
+    Path secret = tempDir.resolve("secret.txt");
+    Files.writeString(secret, "CANARY_SECRET_VALUE");
+    String xml =
+        "<?xml version=\"1.0\"?>"
+            + "<!DOCTYPE foo [ <!ENTITY xxe SYSTEM \""
+            + secret.toUri()
+            + "\"> ]>"
+            + "<root>&xxe;</root>";
+
+    XMLInputFactory factory = 
XmlParserFactoryProducer.createSecureXmlInputFactory();
+    StringBuilder text = new StringBuilder();
+    XMLStreamReader streamReader = factory.createXMLStreamReader(new 
StringReader(xml));
+    try {
+      while (streamReader.hasNext()) {
+        int event = streamReader.next();
+        if (event == XMLStreamConstants.CHARACTERS || event == 
XMLStreamConstants.CDATA) {
+          text.append(streamReader.getText());
+        }
+      }
+    } catch (XMLStreamException e) {
+      // Expected when DTD processing is disabled
+      assertFalse(text.toString().contains("CANARY_SECRET_VALUE"));
+      return;
+    } finally {
+      streamReader.close();
+    }
+
+    assertFalse(
+        text.toString().contains("CANARY_SECRET_VALUE"),
+        "external entity content must not appear in the parse result");
+  }
 }
diff --git 
a/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/staxpoi/StaxUtil.java
 
b/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/staxpoi/StaxUtil.java
index c340f10930..b312706370 100644
--- 
a/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/staxpoi/StaxUtil.java
+++ 
b/plugins/transforms/excel/src/main/java/org/apache/hop/pipeline/transforms/excelinput/staxpoi/StaxUtil.java
@@ -18,6 +18,7 @@
 package org.apache.hop.pipeline.transforms.excelinput.staxpoi;
 
 import javax.xml.stream.XMLInputFactory;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
 import org.apache.poi.ss.SpreadsheetVersion;
 
 public class StaxUtil {
@@ -67,10 +68,6 @@ public class StaxUtil {
   }
 
   public static final XMLInputFactory safeXMLInputFactory() {
-    XMLInputFactory factory = XMLInputFactory.newInstance();
-    // To prevent from XXE attacks
-    factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
-    factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
Boolean.FALSE);
-    return factory;
+    return XmlParserFactoryProducer.createSecureXmlInputFactory();
   }
 }
diff --git 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
index 3c5f68e0fd..3ce7a7ec68 100644
--- 
a/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
+++ 
b/plugins/transforms/webservices/src/main/java/org/apache/hop/pipeline/transforms/webservices/WebService.java
@@ -36,7 +36,6 @@ import java.util.Date;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
-import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.stream.XMLInputFactory;
@@ -906,9 +905,7 @@ public class WebService extends 
BaseTransform<WebServiceMeta, WebServiceData> {
 
     // TODO Very empirical : see if we can do something better here
     try {
-      XMLInputFactory vFactory = XMLInputFactory.newInstance();
-      vFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
-      vFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+      XMLInputFactory vFactory = 
XmlParserFactoryProducer.createSecureXmlInputFactory();
       XMLStreamReader vReader = vFactory.createXMLStreamReader(stringReader);
 
       Object[] outputRowData = 
RowDataUtil.allocateRowData(data.outputRowMeta.size());
diff --git 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/advancedxmloutput/AdvancedXmlOutput.java
 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/advancedxmloutput/AdvancedXmlOutput.java
index 1254842928..2bf45a3ee0 100644
--- 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/advancedxmloutput/AdvancedXmlOutput.java
+++ 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/advancedxmloutput/AdvancedXmlOutput.java
@@ -48,6 +48,7 @@ import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowDataUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
 import org.apache.hop.pipeline.PipelineMeta;
@@ -64,7 +65,8 @@ public class AdvancedXmlOutput extends 
BaseTransform<AdvancedXmlOutputMeta, Adva
 
   private static final String EOL = "\n";
   private static final XMLOutputFactory XML_OUT_FACTORY = 
XMLOutputFactory.newInstance();
-  private static final XMLInputFactory XML_IN_FACTORY = 
createSecureInputFactory();
+  private static final XMLInputFactory XML_IN_FACTORY =
+      XmlParserFactoryProducer.createSecureXmlInputFactory();
 
   /** Writes every byte to two underlying streams (e.g. file + in-memory 
capture). */
   private static final class TeeOutputStream extends OutputStream {
@@ -990,13 +992,6 @@ public class AdvancedXmlOutput extends 
BaseTransform<AdvancedXmlOutputMeta, Adva
     return s;
   }
 
-  private static XMLInputFactory createSecureInputFactory() {
-    XMLInputFactory f = XMLInputFactory.newInstance();
-    f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
-    f.setProperty(XMLInputFactory.SUPPORT_DTD, false);
-    return f;
-  }
-
   /** Test hook: returns the current data object's writer (so unit tests can 
inject a mock). */
   protected XMLStreamWriter getWriter() {
     return data == null ? null : data.writer;
diff --git 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStream.java
 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStream.java
index 7550606ff6..99a32289ff 100644
--- 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStream.java
+++ 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStream.java
@@ -23,7 +23,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
-import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamConstants;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.events.Attribute;
@@ -42,6 +41,7 @@ import org.apache.hop.core.row.RowDataUtil;
 import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.lineage.LineageFileIoEmitter;
 import org.apache.hop.lineage.model.FileIoOperation;
@@ -651,7 +651,7 @@ public class XmlInputStream extends 
BaseTransform<XmlInputStreamMeta, XmlInputSt
   @Override
   public boolean init() {
     if (super.init()) {
-      data.staxInstance = XMLInputFactory.newInstance(); // could select the 
parser later on
+      data.staxInstance = 
XmlParserFactoryProducer.createSecureXmlInputFactory();
       data.staxInstance.setProperty("javax.xml.stream.isCoalescing", false);
       data.filenr = 0;
       if 
(getPipelineMeta().findPreviousTransforms(getTransformMeta()).isEmpty()
diff --git 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStreamTest.java
 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStreamTest.java
index b02a63a171..94142f3731 100644
--- 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStreamTest.java
+++ 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xmlinputstream/XmlInputStreamTest.java
@@ -17,6 +17,7 @@
 package org.apache.hop.pipeline.transforms.xml.xmlinputstream;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -350,6 +351,33 @@ class XmlInputStreamTest {
     assertEquals("other data", rl.getWritten().get(3)[3]);
   }
 
+  @Test
+  void doesNotResolveExternalEntities() throws Exception {
+    File secret = File.createTempFile("xxe-secret", ".txt");
+    secret.deleteOnExit();
+    try (Writer writer = new PrintWriter(secret, "UTF8")) {
+      writer.write("CANARY_SECRET_VALUE");
+    }
+
+    String xml =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+            + "<!DOCTYPE root [ <!ENTITY xxe SYSTEM \""
+            + secret.toURI()
+            + "\"> ]>"
+            + "<root>&xxe;</root>";
+    xmlInputStreamMeta.setFilename(createTestFile(xml));
+
+    assertThrows(HopException.class, this::doTest);
+
+    for (Object[] row : rl.getWritten()) {
+      for (Object cell : row) {
+        if (cell instanceof String value) {
+          assertFalse(value.contains("CANARY_SECRET_VALUE"));
+        }
+      }
+    }
+  }
+
   private void doTest() throws HopException {
     XmlInputStream xmlInputStream =
         new XmlInputStream(

Reply via email to