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 4719f1e350 Issue #3270 : Restrict external access when building XSD 
validator schemas (#8026)
4719f1e350 is described below

commit 4719f1e350c5665535fccadcc2de7460048e0413
Author: vbhanuchander-lang <[email protected]>
AuthorDate: Thu Aug 20 04:39:59 2026 -0400

    Issue #3270 : Restrict external access when building XSD validator schemas 
(#8026)
    
    The XSD validator transform and action hardened the Validator against XXE
    but left the SchemaFactory that produces it unconfigured. The factory is
    what resolves the schema document itself, so a schema was free to pull in
    a DTD or another schema over the network before validation began, and a
    document using xsi:schemaLocation could choose that location itself.
    
    Add XmlParserFactoryProducer.createSecureSchemaFactory, alongside the
    existing secure DocumentBuilderFactory and SAXParserFactory helpers, and
    use it from both call sites. External schema access is restricted to the
    local file system rather than denied outright: FEATURE_SECURE_PROCESSING
    alone makes Xerces refuse every external schema reference, which would
    break xs:include and xs:import of local schema documents. Both call sites
    keep honouring their existing allowExternalEntities option, so a pipeline
    or workflow that deliberately relies on remote schemas is unaffected.
---
 .../hop/core/xml/XmlParserFactoryProducer.java     | 35 ++++++++++++
 .../java/org/apache/hop/core/xml/XmlUtilsTest.java | 64 ++++++++++++++++++++++
 .../actions/xml/xsdvalidator/XsdValidator.java     | 10 +++-
 .../transforms/xml/xsdvalidator/XsdValidator.java  |  9 ++-
 4 files changed, 116 insertions(+), 2 deletions(-)

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 b39f496fff..07db9f2751 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.validation.SchemaFactory;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.core.util.EnvUtil;
@@ -28,6 +29,14 @@ import org.xml.sax.SAXNotRecognizedException;
 import org.xml.sax.SAXNotSupportedException;
 
 public class XmlParserFactoryProducer {
+
+  /**
+   * Value for {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} that keeps schema 
resolution on the local
+   * file system. {@code xs:include} and {@code xs:import} of a local schema 
document still resolve,
+   * while a fetch over http, https or ftp is refused.
+   */
+  private static final String LOCAL_FILE_ACCESS_ONLY = "file";
+
   private XmlParserFactoryProducer() {
     // Static class
   }
@@ -108,4 +117,30 @@ public class XmlParserFactoryProducer {
 
     return factory;
   }
+
+  /**
+   * Creates an instance of {@link SchemaFactory} class with enabled {@link
+   * XMLConstants#FEATURE_SECURE_PROCESSING} property, external DTD access 
denied and external
+   * schema access restricted to the local file system.
+   *
+   * <p>Hardening the factory matters separately from hardening the {@link
+   * javax.xml.validation.Validator} it produces: the factory is what resolves 
the schema document
+   * itself, so without these restrictions a schema is free to pull in a DTD 
or another schema over
+   * the network before any validation begins.
+   *
+   * @param schemaLanguage the schema language URI, e.g. {@link 
XMLConstants#W3C_XML_SCHEMA_NS_URI}
+   * @throws SAXNotRecognizedException When the underlying parser does not 
recognize the property
+   *     name.
+   * @throws SAXNotSupportedException When the underlying parser recognizes 
the property name but
+   *     doesn't support the property.
+   */
+  public static SchemaFactory createSecureSchemaFactory(String schemaLanguage)
+      throws SAXNotRecognizedException, SAXNotSupportedException {
+    SchemaFactory factory = SchemaFactory.newInstance(schemaLanguage);
+    factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+    factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+    factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, 
LOCAL_FILE_ACCESS_ONLY);
+
+    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 87df2de394..d7682559ae 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
@@ -17,12 +17,20 @@
 
 package org.apache.hop.core.xml;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.File;
+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.validation.SchemaFactory;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.xml.sax.SAXException;
 
 /** Unit test for {@link XmlParserFactoryProducer} */
 class XmlUtilsTest {
@@ -40,4 +48,60 @@ class XmlUtilsTest {
 
     
assertTrue(saxParserFactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
   }
+
+  @Test
+  void secureFeatureEnabledAfterSchemaFactoryCreation() throws Exception {
+    SchemaFactory schemaFactory =
+        
XmlParserFactoryProducer.createSecureSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+    
assertTrue(schemaFactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+  }
+
+  // The accessExternalDTD / accessExternalSchema values cannot be read back 
from Xerces'
+  // XMLSchemaFactory -- setProperty accepts them but getProperty rejects them 
as unrecognized -- so
+  // the two tests below assert their effect instead.
+
+  @Test
+  void secureSchemaFactoryRefusesRemoteSchemaReference(@TempDir Path tempDir) 
throws Exception {
+    // The host is never contacted: access is refused by the 
accessExternalSchema restriction
+    // before any network I/O is attempted.
+    Path schema = tempDir.resolve("remote-include.xsd");
+    Files.writeString(
+        schema,
+        "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>"
+            + "<xs:include 
schemaLocation=\"http://hop.apache.org.invalid/stolen.xsd\"/>"
+            + "</xs:schema>");
+
+    SchemaFactory schemaFactory =
+        
XmlParserFactoryProducer.createSecureSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+    SAXException e =
+        assertThrows(SAXException.class, () -> 
schemaFactory.newSchema(schema.toFile()));
+    assertTrue(
+        e.getMessage().contains("access is not allowed"),
+        "expected an access restriction failure but got: " + e.getMessage());
+  }
+
+  @Test
+  void secureSchemaFactoryStillResolvesLocalSchemaReference(@TempDir Path 
tempDir)
+      throws Exception {
+    // Restricting external access must not break multi-file schemas on the 
local file system.
+    Files.writeString(
+        tempDir.resolve("included.xsd"),
+        "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>"
+            + "<xs:element name=\"included\" type=\"xs:string\"/>"
+            + "</xs:schema>");
+    File including = tempDir.resolve("including.xsd").toFile();
+    Files.writeString(
+        including.toPath(),
+        "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>"
+            + "<xs:include schemaLocation=\"included.xsd\"/>"
+            + "<xs:element name=\"root\" type=\"xs:string\"/>"
+            + "</xs:schema>");
+
+    SchemaFactory schemaFactory =
+        
XmlParserFactoryProducer.createSecureSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+    assertDoesNotThrow(() -> schemaFactory.newSchema(including));
+  }
 }
diff --git 
a/plugins/actions/xml/src/main/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidator.java
 
b/plugins/actions/xml/src/main/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidator.java
index 8e541b818b..49cc51906d 100644
--- 
a/plugins/actions/xml/src/main/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidator.java
+++ 
b/plugins/actions/xml/src/main/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidator.java
@@ -26,6 +26,7 @@ import static 
org.apache.hop.workflow.action.validator.AndValidator.putValidator
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import javax.xml.XMLConstants;
 import javax.xml.transform.Source;
 import javax.xml.transform.stream.StreamSource;
 import javax.xml.validation.Schema;
@@ -43,6 +44,7 @@ import org.apache.hop.core.exception.HopFileException;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
@@ -133,8 +135,14 @@ public class XsdValidator extends ActionBase implements 
Cloneable, IAction {
       String realxmlfilename = getRealxmlfilename();
       xmlfile = getFile(realxmlfilename);
 
+      // The factory resolves the schema document itself, so it needs the same 
external-access
+      // restrictions as the validator it produces. Honour the action's own 
opt-in so a workflow
+      // that deliberately relies on remote schemas keeps working.
       SchemaFactory factorytXSDValidator1 =
-          SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema";);
+          isAllowExternalEntities()
+              ? SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
+              : XmlParserFactoryProducer.createSecureSchemaFactory(
+                  XMLConstants.W3C_XML_SCHEMA_NS_URI);
 
       if (xsdSource.equals(SPECIFY_FILENAME)) {
         validateNonNullFileName(xsdFilename, 
"ActionXSDValidator.XsdFileNotNull.Label", result);
diff --git 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidator.java
 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidator.java
index 7f02c70f2d..4aa6fbc132 100644
--- 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidator.java
+++ 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidator.java
@@ -34,6 +34,7 @@ import org.apache.hop.core.exception.HopFileException;
 import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.row.RowDataUtil;
 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;
@@ -132,8 +133,14 @@ public class XsdValidator extends 
BaseTransform<XsdValidatorMeta, XsdValidatorDa
 
       try {
 
+        // The factory resolves the schema document itself, so it needs the 
same external-access
+        // restrictions as the validator it produces. Honour the transform's 
own opt-in so a
+        // pipeline that deliberately relies on remote schemas keeps working.
         SchemaFactory factoryXSDValidator =
-            SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+            meta.isAllowExternalEntities()
+                ? SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
+                : XmlParserFactoryProducer.createSecureSchemaFactory(
+                    XMLConstants.W3C_XML_SCHEMA_NS_URI);
 
         // Get XML stream
         Source sourceXML = getSourceXML(getInputRowMeta().getString(row, 
data.xmlindex));

Reply via email to