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));