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

bamaer 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 816303fb74 Issue #8367 : Disable external entity resolution by default 
in XSD va… (#8368)
816303fb74 is described below

commit 816303fb74fa46898dabe8557b3ea09070df405f
Author: Bart Maertens <[email protected]>
AuthorDate: Wed Sep 16 20:49:50 2026 +0200

    Issue #8367 : Disable external entity resolution by default in XSD va… 
(#8368)
    
    * Issue #8367 : Disable external entity resolution by default in XSD 
validation
    
    Fixes #8367
    
    The XSD Validator transform and action defaulted
    ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION_DEFAULT to "true", so the 
hardened
    code path both of them already carried was never taken. Both constants are 
now
    "false"; the checkbox in either dialog and the system property still 
override
    it.
    
    Flipping the default exposed that the hardened path did not work on Hop's 
own
    classpath. createSecureSchemaFactory() sets ACCESS_EXTERNAL_DTD and
    ACCESS_EXTERNAL_SCHEMA, and Xerces' XMLSchemaFactory - the SchemaFactory 
that
    xercesImpl contributes to the Hop runtime - throws 
SAXNotRecognizedException for
    both, so every validation failed with "Property
    'http://javax.xml.XMLConstants/property/accessExternalDTD' is not 
recognized."
    hop-core's tests missed it because core has no Xerces and gets the JDK 
factory.
    The two property calls are now best-effort, the same way
    createSecureXmlInputFactory() already handles Woodstox, and when they are
    unrecognized an LSResourceResolver enforces the identical policy: external 
DTDs
    refused, xs:include / xs:import allowed on file: only, anything else refused
    without a fetch.
    
    Tests: SecureSchemaFactoryTest runs on the transform module's classpath, 
where
    Xerces is present, and uses a loopback HTTP server to assert the secure 
factory
    issues zero requests while a plain SchemaFactory issues one. Integration 
tests
    0021 (transform) and 0022 (action) in the xml project drive the same three 
cases
    end to end against the suite's file server.
    
    Note that this is a behaviour change for newly created transforms and 
actions,
    and that XSD source "is defined inside the XML" stops working while external
    entities are off.
    
    * Issue #8367 : Document the three effects of disabling external entity 
resolution
---
 .../hop/core/xml/XmlParserFactoryProducer.java     | 165 +++++++++-
 .../integration-tests/integration-tests-xml.yaml   |   5 +-
 .../integration-tests/resource/xml/xxe-payload.txt |   1 +
 .../pages/pipeline/transforms/xsdvalidator.adoc    |  12 +-
 .../ROOT/pages/workflow/actions/xsdvalidator.adoc  |  15 +-
 integration-tests/xml/0021-xsd-validator-xxe.hpl   | 343 +++++++++++++++++++++
 .../xml/datasets/golden-xsd-validator-xxe.csv      |   4 +
 integration-tests/xml/files/xxe-employee.xml       |  33 ++
 integration-tests/xml/files/xxe-employee.xsd       |  32 ++
 integration-tests/xml/files/xxe-safe-employee.xml  |  29 ++
 .../xml/main-0021-xsd-validator-xxe.hwf            |  79 +++++
 .../xml/main-0022-xsd-validator-action-xxe.hwf     | 168 ++++++++++
 .../metadata/dataset/golden-xsd-validator-xxe.json |  24 ++
 .../unit-test/0021-xsd-validator-xxe UNIT.json     |  33 ++
 .../actions/xml/xsdvalidator/XsdValidator.java     |   2 +-
 .../actions/xml/xsdvalidator/XsdValidatorTest.java |  30 ++
 .../xml/xsdvalidator/XsdValidatorMeta.java         |   2 +-
 .../xml/xsdvalidator/SecureSchemaFactoryTest.java  | 182 +++++++++++
 .../xml/xsdvalidator/XsdValidatorMetaTest.java     |   5 +
 19 files changed, 1155 insertions(+), 9 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 84c3d3ada6..21d622927b 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
@@ -17,6 +17,10 @@
 
 package org.apache.hop.core.xml;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.net.URI;
 import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -26,6 +30,8 @@ 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;
+import org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSResourceResolver;
 import org.xml.sax.SAXNotRecognizedException;
 import org.xml.sax.SAXNotSupportedException;
 
@@ -129,6 +135,10 @@ public class XmlParserFactoryProducer {
    * itself, so without these restrictions a schema is free to pull in a DTD 
or another schema over
    * the network before any validation begins.
    *
+   * <p>Not every {@link SchemaFactory} implementation recognizes the JAXP 
access properties --
+   * Xerces' standalone {@code XMLSchemaFactory} does not. For those, a 
resource resolver enforces
+   * the same local-file-only policy.
+   *
    * @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.
@@ -139,12 +149,163 @@ public class XmlParserFactoryProducer {
       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);
+    try {
+      factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+      factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, 
LOCAL_FILE_ACCESS_ONLY);
+    } catch (SAXNotRecognizedException | SAXNotSupportedException e) {
+      // Xerces' standalone XMLSchemaFactory, the schema factory on Hop's 
runtime classpath, does
+      // not know the JAXP access properties. Refusing the factory outright 
would break every
+      // validation, so apply the same "local file system only" policy through 
a resource resolver.
+      factory.setResourceResolver(new LocalFileOnlyResourceResolver());
+    }
 
     return factory;
   }
 
+  /**
+   * Resolves {@code xs:include} and {@code xs:import} on the local file 
system only, and refuses
+   * external DTDs outright. It is the fallback for parsers that do not 
recognize {@link
+   * XMLConstants#ACCESS_EXTERNAL_DTD} and {@link 
XMLConstants#ACCESS_EXTERNAL_SCHEMA}: a schema
+   * reference that resolves to any other scheme (http, https, ftp, jar, ...) 
is refused instead of
+   * fetched.
+   */
+  private static class LocalFileOnlyResourceResolver implements 
LSResourceResolver {
+
+    /** The {@code type} {@link LSResourceResolver} gets for an external DTD 
or entity. */
+    private static final String DTD_RESOURCE_TYPE = 
"http://www.w3.org/TR/REC-xml";;
+
+    @Override
+    public LSInput resolveResource(
+        String type, String namespaceUri, String publicId, String systemId, 
String baseUri) {
+      if (systemId == null) {
+        // Nothing to fetch: let the parser resolve it as it normally would.
+        return null;
+      }
+      if (DTD_RESOURCE_TYPE.equals(type)) {
+        // Mirrors ACCESS_EXTERNAL_DTD="": no external DTD, local file system 
included.
+        return new RefusedInput(publicId, systemId, baseUri);
+      }
+      return isLocalFile(systemId, baseUri) ? null : new 
RefusedInput(publicId, systemId, baseUri);
+    }
+
+    private boolean isLocalFile(String systemId, String baseUri) {
+      try {
+        URI uri = URI.create(systemId);
+        if (baseUri != null && !uri.isAbsolute()) {
+          uri = URI.create(baseUri).resolve(uri);
+        }
+        // A reference that stays relative is resolved against the schema's 
own location.
+        return !uri.isAbsolute() || 
LOCAL_FILE_ACCESS_ONLY.equalsIgnoreCase(uri.getScheme());
+      } catch (IllegalArgumentException e) {
+        // Not a URI we can reason about, so don't hand it to the parser 
either.
+        return false;
+      }
+    }
+  }
+
+  /**
+   * An {@link LSInput} that fails as soon as the parser reads from it, so a 
blocked reference ends
+   * up as a validation error instead of a network call.
+   */
+  private static class RefusedInput implements LSInput {
+
+    private final String publicId;
+    private final String systemId;
+    private final String baseUri;
+
+    RefusedInput(String publicId, String systemId, String baseUri) {
+      this.publicId = publicId;
+      this.systemId = systemId;
+      this.baseUri = baseUri;
+    }
+
+    @Override
+    public InputStream getByteStream() {
+      return new InputStream() {
+        @Override
+        public int read() throws IOException {
+          throw new IOException(
+              "External schema or DTD access is not allowed, refused to 
resolve: " + systemId);
+        }
+      };
+    }
+
+    @Override
+    public Reader getCharacterStream() {
+      return null;
+    }
+
+    @Override
+    public void setCharacterStream(Reader characterStream) {
+      // Read-only
+    }
+
+    @Override
+    public void setByteStream(InputStream byteStream) {
+      // Read-only
+    }
+
+    @Override
+    public String getStringData() {
+      return null;
+    }
+
+    @Override
+    public void setStringData(String stringData) {
+      // Read-only
+    }
+
+    @Override
+    public String getSystemId() {
+      return systemId;
+    }
+
+    @Override
+    public void setSystemId(String systemId) {
+      // Read-only
+    }
+
+    @Override
+    public String getPublicId() {
+      return publicId;
+    }
+
+    @Override
+    public void setPublicId(String publicId) {
+      // Read-only
+    }
+
+    @Override
+    public String getBaseURI() {
+      return baseUri;
+    }
+
+    @Override
+    public void setBaseURI(String baseUri) {
+      // Read-only
+    }
+
+    @Override
+    public String getEncoding() {
+      return null;
+    }
+
+    @Override
+    public void setEncoding(String encoding) {
+      // Read-only
+    }
+
+    @Override
+    public boolean getCertifiedText() {
+      return false;
+    }
+
+    @Override
+    public void setCertifiedText(boolean certifiedText) {
+      // Read-only
+    }
+  }
+
   /**
    * 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
diff --git a/docker/integration-tests/integration-tests-xml.yaml 
b/docker/integration-tests/integration-tests-xml.yaml
index 3026a0a1ba..4a2f669659 100644
--- a/docker/integration-tests/integration-tests-xml.yaml
+++ b/docker/integration-tests/integration-tests-xml.yaml
@@ -29,4 +29,7 @@ services:
     ports:
       - "8080"
     volumes:
-      - ./resource/xml/test.xsd:/web/test.xsd
\ No newline at end of file
+      - ./resource/xml/test.xsd:/web/test.xsd
+      # Target of the external entity in files/xxe-employee.xml. The XSD 
Validator tests assert
+      # that this file is only ever fetched when external entities are 
explicitly enabled.
+      - ./resource/xml/xxe-payload.txt:/web/xxe-payload.txt
\ No newline at end of file
diff --git a/docker/integration-tests/resource/xml/xxe-payload.txt 
b/docker/integration-tests/resource/xml/xxe-payload.txt
new file mode 100644
index 0000000000..66098ac491
--- /dev/null
+++ b/docker/integration-tests/resource/xml/xxe-payload.txt
@@ -0,0 +1 @@
+xxe-payload-was-fetched
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc
index d24775f620..836dfed4fd 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc
@@ -56,11 +56,21 @@ See also the xref:workflow/actions/xsdvalidator.adoc[XSD 
Validator workflow acti
 * Add validation msg in output : check this if you want to add a validation 
message in the output.
 * Validation msg field: the name of the validation message field.
 
+|Enable external entity for XSD validation a|
+
+Allow the XSD validation to resolve external references (XXE). Unchecked by 
default, which has three effects:
+
+* A `DOCTYPE` declaration or an external entity in the XML being validated is 
refused and the row is reported as invalid.
+* XSD source *The XSD source is defined inside the source XML* does not work: 
the `xsi:schemaLocation` / `xsi:noNamespaceSchemaLocation` reference is not 
resolved, not even when it points at a local file. This option needs the 
checkbox.
+* A schema that pulls in another schema over the network with `xs:include` or 
`xs:import` (for example the W3C `xml.xsd`) needs the checkbox. Local file and 
relative includes keep working without it.
+
+Only check this when the XML documents and the schemas you validate against 
are trusted. The default for new transforms can be changed with the 
`ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION` Java system property.
+
 |XML schema definition a|
 
 XSD source : select one of these options:
 
 * The XSD source is a file, let me specify the filename (specify below in the 
XSD filename field).
 * The XSD source is a file, defined in a field (specify below in the XSD 
filename field).
-* The XSD source is defined inside the source XML.
+* The XSD source is defined inside the source XML. Requires *Enable external 
entity for XSD validation*.
 |===
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/xsdvalidator.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/xsdvalidator.adoc
index 944184ef35..bd3923fc86 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/xsdvalidator.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/xsdvalidator.adoc
@@ -39,12 +39,21 @@ See also the xref:pipeline/transforms/xsdvalidator.adoc[XSD 
Validator pipeline t
 |===
 |Option|Description
 |Action name|The name of the workflow action.
-|Enable external entity for XSD validation|
+|Enable external entity for XSD validation a|
+
+Allow the XSD validation to resolve external references (XXE). Unchecked by 
default, which has three effects:
+
+* A `DOCTYPE` declaration or an external entity in the XML being validated is 
refused and the action fails.
+* XSD sources *none (default)* and *is defined inside XML* do not work: the 
`xsi:schemaLocation` / `xsi:noNamespaceSchemaLocation` reference is not 
resolved, not even when it points at a local file. Both options need the 
checkbox.
+* A schema that pulls in another schema over the network with `xs:include` or 
`xs:import` (for example the W3C `xml.xsd`) needs the checkbox. Local file and 
relative includes keep working without it.
+
+Only check this when the XML documents and the schemas you validate against 
are trusted. The default for new actions can be changed with the 
`ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION` Java system property.
+
 |XSD Source a|The options are
 
-* none (default)
+* none (default) - requires *Enable external entity for XSD validation*
 * is a file, let me specify filename
-* is defined inside XML
+* is defined inside XML - requires *Enable external entity for XSD validation*
 |XML File name|The name of the XML file
 |XSD File name|The name of the XSD file
 |===
diff --git a/integration-tests/xml/0021-xsd-validator-xxe.hpl 
b/integration-tests/xml/0021-xsd-validator-xxe.hpl
new file mode 100644
index 0000000000..01fc7b1442
--- /dev/null
+++ b/integration-tests/xml/0021-xsd-validator-xxe.hpl
@@ -0,0 +1,343 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0021-xsd-validator-xxe</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Issue #8367: the XSD Validator transform must not resolve 
external entities unless it is explicitly told to.</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/14 14:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/14 14:00:00.000</modified_date>
+    <key_for_session_key/>
+    <is_key_private>N</is_key_private>
+  </info>
+  <notepads>
+    <notepad>
+      <note>Three cases share one golden data set.
+
+1 - a document without entities still validates with the secure default, so the
+    hardening does not break ordinary XSD validation.
+2 - the XXE document is refused by the secure default: its DOCTYPE is 
disallowed
+    and the file server is never contacted.
+3 - the same XXE document validates once external entities are enabled, which
+    proves case 2 fails because of the hardening and not because the payload is
+    unreachable.</note>
+      <xloc>32</xloc>
+      <yloc>352</yloc>
+      <width>520</width>
+      <heigth>140</heigth>
+      <fontname>Sans</fontname>
+      <fontsize>10</fontsize>
+      <fontbold>N</fontbold>
+      <fontitalic>N</fontitalic>
+      <fontcolorred>0</fontcolorred>
+      <fontcolorgreen>0</fontcolorgreen>
+      <fontcolorblue>0</fontcolorblue>
+      <backgroundcolorred>255</backgroundcolorred>
+      <backgroundcolorgreen>205</backgroundcolorgreen>
+      <backgroundcolorblue>112</backgroundcolorblue>
+      <bordercolorred>100</bordercolorred>
+      <bordercolorgreen>100</bordercolorgreen>
+      <bordercolorblue>100</bordercolorblue>
+    </notepad>
+  </notepads>
+  <order>
+    <hop>
+      <from>Get safe xml</from>
+      <to>XSD validator secure default</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Get xxe xml</from>
+      <to>XSD validator secure default</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Get xxe xml for allowed validator</from>
+      <to>XSD validator external entities allowed</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>XSD validator secure default</from>
+      <to>Sort by case</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>XSD validator external entities allowed</from>
+      <to>Sort by case</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Sort by case</from>
+      <to>Dummy (do nothing)</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Get safe xml</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>xml-file</name>
+        <variable>${PROJECT_HOME}/files/xxe-safe-employee.xml</variable>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+      </field>
+      <field>
+        <name>case</name>
+        <variable>1-safe-xml-secure-default</variable>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Get xxe xml</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>xml-file</name>
+        <variable>${PROJECT_HOME}/files/xxe-employee.xml</variable>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+      </field>
+      <field>
+        <name>case</name>
+        <variable>2-xxe-secure-default</variable>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>176</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Get xxe xml for allowed validator</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>xml-file</name>
+        <variable>${PROJECT_HOME}/files/xxe-employee.xml</variable>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+      </field>
+      <field>
+        <name>case</name>
+        <variable>3-xxe-external-entities-allowed</variable>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>272</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>XSD validator secure default</name>
+    <type>XSDValidator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <xdsfilename>${PROJECT_HOME}/files/xxe-employee.xsd</xdsfilename>
+    <xmlstream>xml-file</xmlstream>
+    <resultfieldname>result</resultfieldname>
+    <addvalidationmsg>Y</addvalidationmsg>
+    <validationmsgfield>ValidationMsgField</validationmsgfield>
+    <ifxmlunvalid/>
+    <ifxmlvalid/>
+    <outputstringfield>N</outputstringfield>
+    <xmlsourcefile>Y</xmlsourcefile>
+    <xsddefinedfield/>
+    <xsdsource>filename</xsdsource>
+    <allowExternalEntities>N</allowExternalEntities>
+    <attributes/>
+    <GUI>
+      <xloc>352</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>XSD validator external entities allowed</name>
+    <type>XSDValidator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <xdsfilename>${PROJECT_HOME}/files/xxe-employee.xsd</xdsfilename>
+    <xmlstream>xml-file</xmlstream>
+    <resultfieldname>result</resultfieldname>
+    <addvalidationmsg>Y</addvalidationmsg>
+    <validationmsgfield>ValidationMsgField</validationmsgfield>
+    <ifxmlunvalid/>
+    <ifxmlvalid/>
+    <outputstringfield>N</outputstringfield>
+    <xmlsourcefile>Y</xmlsourcefile>
+    <xsddefinedfield/>
+    <xsdsource>filename</xsdsource>
+    <allowExternalEntities>Y</allowExternalEntities>
+    <attributes/>
+    <GUI>
+      <xloc>352</xloc>
+      <yloc>272</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Sort by case</name>
+    <type>SortRows</type>
+    <description>Two validators feed this hop, so sort to keep the golden 
comparison deterministic.</description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <directory>${java.io.tmpdir}</directory>
+    <prefix>out</prefix>
+    <sort_size>1000000</sort_size>
+    <free_memory/>
+    <compress>N</compress>
+    <compress_variable/>
+    <unique_rows>N</unique_rows>
+    <fields>
+      <field>
+        <name>case</name>
+        <ascending>Y</ascending>
+        <case_sensitive>Y</case_sensitive>
+        <collator_enabled>N</collator_enabled>
+        <collator_strength>0</collator_strength>
+        <presorted>N</presorted>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>592</xloc>
+      <yloc>192</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Dummy (do nothing)</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>800</xloc>
+      <yloc>192</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/xml/datasets/golden-xsd-validator-xxe.csv 
b/integration-tests/xml/datasets/golden-xsd-validator-xxe.csv
new file mode 100644
index 0000000000..c830fd9b9d
--- /dev/null
+++ b/integration-tests/xml/datasets/golden-xsd-validator-xxe.csv
@@ -0,0 +1,4 @@
+case,result
+1-safe-xml-secure-default,Y
+2-xxe-secure-default,N
+3-xxe-external-entities-allowed,Y
diff --git a/integration-tests/xml/files/xxe-employee.xml 
b/integration-tests/xml/files/xxe-employee.xml
new file mode 100644
index 0000000000..2078aa218b
--- /dev/null
+++ b/integration-tests/xml/files/xxe-employee.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<!--
+  An XML External Entity (XXE) payload: the email element pulls its content 
from the integration
+  test file server. With external entities disabled - the default since issue 
#8367 - the DOCTYPE
+  is refused and the file server is never contacted, so validation fails. 
Enabling external
+  entities on the transform or action resolves the entity and the document 
validates.
+-->
+<!DOCTYPE employee [
+  <!ENTITY xxe SYSTEM "http://file-server:8080/xxe-payload.txt";>
+]>
+<employee>
+  <firstname>vimal</firstname>
+  <lastname>jaiswal</lastname>
+  <email>&xxe;</email>
+</employee>
diff --git a/integration-tests/xml/files/xxe-employee.xsd 
b/integration-tests/xml/files/xxe-employee.xsd
new file mode 100644
index 0000000000..3cef2680f3
--- /dev/null
+++ b/integration-tests/xml/files/xxe-employee.xsd
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema";>
+
+  <xs:element name="employee">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element name="firstname" type="xs:string"/>
+        <xs:element name="lastname" type="xs:string"/>
+        <xs:element name="email" type="xs:string"/>
+      </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+
+</xs:schema>
diff --git a/integration-tests/xml/files/xxe-safe-employee.xml 
b/integration-tests/xml/files/xxe-safe-employee.xml
new file mode 100644
index 0000000000..598ee6e322
--- /dev/null
+++ b/integration-tests/xml/files/xxe-safe-employee.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<!--
+  The same document as xxe-employee.xml without the external entity. It 
validates against
+  xxe-employee.xsd whether or not external entities are allowed, which keeps 
the hardening honest:
+  disabling external entities must not break ordinary XSD validation.
+-->
+<employee>
+  <firstname>vimal</firstname>
+  <lastname>jaiswal</lastname>
+  <email>[email protected]</email>
+</employee>
diff --git a/integration-tests/xml/main-0021-xsd-validator-xxe.hwf 
b/integration-tests/xml/main-0021-xsd-validator-xxe.hwf
new file mode 100644
index 0000000000..2471ae954f
--- /dev/null
+++ b/integration-tests/xml/main-0021-xsd-validator-xxe.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0021-xsd-validator-xxe</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description>Issue #8367: the XSD Validator transform does not resolve 
external entities by default.</description>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/14 14:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/14 14:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <hour>12</hour>
+      <minutes>0</minutes>
+      <weekDay>1</weekDay>
+      <DayOfMonth>1</DayOfMonth>
+      <parallel>N</parallel>
+      <xloc>128</xloc>
+      <yloc>112</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0021-xsd-validator-xxe UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>320</xloc>
+      <yloc>112</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/xml/main-0022-xsd-validator-action-xxe.hwf 
b/integration-tests/xml/main-0022-xsd-validator-action-xxe.hwf
new file mode 100644
index 0000000000..5e5c3485d1
--- /dev/null
+++ b/integration-tests/xml/main-0022-xsd-validator-action-xxe.hwf
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0022-xsd-validator-action-xxe</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description>Issue #8367: the XSD Validator action does not resolve external 
entities by default.</description>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/14 14:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/14 14:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <hour>12</hour>
+      <minutes>0</minutes>
+      <weekDay>1</weekDay>
+      <DayOfMonth>1</DayOfMonth>
+      <parallel>N</parallel>
+      <xloc>80</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>safe xml validates with secure default</name>
+      <description>A document without entities must still validate once 
external entities are off.</description>
+      <type>XSD_VALIDATOR</type>
+      <attributes/>
+      <xsdsource>filename</xsdsource>
+      <xmlfilename>${PROJECT_HOME}/files/xxe-safe-employee.xml</xmlfilename>
+      <xsdfilename>${PROJECT_HOME}/files/xxe-employee.xsd</xsdfilename>
+      <allowExternalEntities>N</allowExternalEntities>
+      <parallel>N</parallel>
+      <xloc>288</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>xxe xml is refused by secure default</name>
+      <description>The DOCTYPE is disallowed, so this action fails and the 
file server is never contacted.</description>
+      <type>XSD_VALIDATOR</type>
+      <attributes/>
+      <xsdsource>filename</xsdsource>
+      <xmlfilename>${PROJECT_HOME}/files/xxe-employee.xml</xmlfilename>
+      <xsdfilename>${PROJECT_HOME}/files/xxe-employee.xsd</xsdfilename>
+      <allowExternalEntities>N</allowExternalEntities>
+      <parallel>N</parallel>
+      <xloc>544</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>xxe xml validates when entities are allowed</name>
+      <description>Proves the previous action fails because of the hardening, 
not an unreachable payload.</description>
+      <type>XSD_VALIDATOR</type>
+      <attributes/>
+      <xsdsource>filename</xsdsource>
+      <xmlfilename>${PROJECT_HOME}/files/xxe-employee.xml</xmlfilename>
+      <xsdfilename>${PROJECT_HOME}/files/xxe-employee.xsd</xsdfilename>
+      <allowExternalEntities>Y</allowExternalEntities>
+      <parallel>N</parallel>
+      <xloc>816</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Success</name>
+      <description/>
+      <type>SUCCESS</type>
+      <attributes/>
+      <parallel>N</parallel>
+      <xloc>1056</xloc>
+      <yloc>48</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Abort workflow</name>
+      <description/>
+      <type>ABORT</type>
+      <attributes/>
+      <parallel>N</parallel>
+      <xloc>544</xloc>
+      <yloc>176</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>safe xml validates with secure default</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>safe xml validates with secure default</from>
+      <to>xxe xml is refused by secure default</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>safe xml validates with secure default</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>xxe xml is refused by secure default</from>
+      <to>xxe xml validates when entities are allowed</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>xxe xml is refused by secure default</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>xxe xml validates when entities are allowed</from>
+      <to>Success</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>xxe xml validates when entities are allowed</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/xml/metadata/dataset/golden-xsd-validator-xxe.json 
b/integration-tests/xml/metadata/dataset/golden-xsd-validator-xxe.json
new file mode 100644
index 0000000000..d64527bf9a
--- /dev/null
+++ b/integration-tests/xml/metadata/dataset/golden-xsd-validator-xxe.json
@@ -0,0 +1,24 @@
+{
+  "base_filename": "golden-xsd-validator-xxe.csv",
+  "name": "golden-xsd-validator-xxe",
+  "description": "Expected XSD Validator results for the external entity (XXE) 
cases of issue #8367",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_format": "",
+      "field_name": "case"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 4,
+      "field_precision": -1,
+      "field_format": "",
+      "field_name": "result"
+    }
+  ],
+  "folder_name": ""
+}
diff --git a/integration-tests/xml/metadata/unit-test/0021-xsd-validator-xxe 
UNIT.json b/integration-tests/xml/metadata/unit-test/0021-xsd-validator-xxe 
UNIT.json
new file mode 100644
index 0000000000..5bde7da5b2
--- /dev/null
+++ b/integration-tests/xml/metadata/unit-test/0021-xsd-validator-xxe UNIT.json 
@@ -0,0 +1,33 @@
+{
+  "variableValues": [],
+  "database_replacements": [],
+  "autoOpening": true,
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "case",
+          "data_set_field": "case"
+        },
+        {
+          "transform_field": "result",
+          "data_set_field": "result"
+        }
+      ],
+      "field_order": [
+        "case",
+        "result"
+      ],
+      "transform_name": "Dummy (do nothing)",
+      "data_set_name": "golden-xsd-validator-xxe"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0021-xsd-validator-xxe UNIT",
+  "description": "",
+  "trans_test_tweaks": [],
+  "persist_filename": "",
+  "pipeline_filename": "./0021-xsd-validator-xxe.hpl",
+  "test_type": "UNIT_TEST"
+}
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 627735af32..238179a25e 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
@@ -73,7 +73,7 @@ public class XsdValidator extends ActionBase implements 
Cloneable, IAction {
 
   public static final String ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION =
       "ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION";
-  public static final String 
ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION_DEFAULT = "true";
+  public static final String 
ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION_DEFAULT = "false";
 
   private static final String YES = "Y";
 
diff --git 
a/plugins/actions/xml/src/test/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidatorTest.java
 
b/plugins/actions/xml/src/test/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidatorTest.java
new file mode 100644
index 0000000000..ed0cef19c1
--- /dev/null
+++ 
b/plugins/actions/xml/src/test/java/org/apache/hop/workflow/actions/xml/xsdvalidator/XsdValidatorTest.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.workflow.actions.xml.xsdvalidator;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.junit.jupiter.api.Test;
+
+class XsdValidatorTest {
+
+  @Test
+  void testExternalEntitiesAreDisabledByDefault() {
+    assertFalse(new XsdValidator().isAllowExternalEntities());
+  }
+}
diff --git 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMeta.java
 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMeta.java
index 37c051cdb1..7af7e3cd72 100644
--- 
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMeta.java
+++ 
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMeta.java
@@ -59,7 +59,7 @@ public class XsdValidatorMeta extends 
BaseTransformMeta<XsdValidator, XsdValidat
 
   public static final String ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION =
       "ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION";
-  public static final String 
ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION_DEFAULT = "true";
+  public static final String 
ALLOW_EXTERNAL_ENTITIES_FOR_XSD_VALIDATION_DEFAULT = "false";
 
   public static final String SPECIFY_FILENAME = "filename";
   public static final String SPECIFY_FIELDNAME = "fieldname";
diff --git 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/SecureSchemaFactoryTest.java
 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/SecureSchemaFactoryTest.java
new file mode 100644
index 0000000000..aa1ec82139
--- /dev/null
+++ 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/SecureSchemaFactoryTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.xml.xsdvalidator;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.sun.net.httpserver.HttpServer;
+import java.io.File;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.xml.XMLConstants;
+import javax.xml.validation.SchemaFactory;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXParseException;
+
+/**
+ * The XSD Validator transform and action hand external entity resolution to 
{@link
+ * XmlParserFactoryProducer#createSecureSchemaFactory(String)}. This module 
puts Xerces on the
+ * classpath the way the Hop runtime does, and Xerces' {@code 
XMLSchemaFactory} does not recognize
+ * the JAXP external access properties -- so the restrictions have to be 
verified here, not only
+ * against the JDK implementation used in hop-core's own tests.
+ */
+class SecureSchemaFactoryTest {
+
+  private static final String REMOTE_SCHEMA =
+      "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>"
+          + "<xs:element name=\"remote\" type=\"xs:string\"/>"
+          + "</xs:schema>";
+
+  @Test
+  void secureSchemaFactoryIsTheXercesImplementation() throws Exception {
+    SchemaFactory schemaFactory =
+        
XmlParserFactoryProducer.createSecureSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+    assertEquals(
+        "org.apache.xerces.jaxp.validation.XMLSchemaFactory", 
schemaFactory.getClass().getName());
+    
assertTrue(schemaFactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+  }
+
+  @Test
+  void secureSchemaFactoryDoesNotFetchRemoteSchemaReference(@TempDir Path 
tempDir)
+      throws Exception {
+    AtomicInteger requests = new AtomicInteger();
+    HttpServer server = startSchemaServer(requests);
+    try {
+      File including = writeIncludingSchema(tempDir, "including-secure.xsd", 
server);
+
+      SchemaFactory schemaFactory =
+          
XmlParserFactoryProducer.createSecureSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+      // Xerces reports an unresolvable xs:include as a non-fatal error, so 
collect it rather than
+      // expecting newSchema() to throw.
+      List<SAXParseException> problems = new ArrayList<>();
+      schemaFactory.setErrorHandler(collectingErrorHandler(problems));
+
+      schemaFactory.newSchema(including);
+
+      assertEquals(0, requests.get(), "the secure schema factory contacted the 
remote host");
+      assertTrue(!problems.isEmpty(), "expected the blocked xs:include to be 
reported");
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void plainSchemaFactoryDoesFetchRemoteSchemaReference(@TempDir Path tempDir) 
throws Exception {
+    // Guards the test above: without the hardening the very same xs:include 
is fetched, so a zero
+    // request count there really is the restriction at work and not an 
unreachable server.
+    AtomicInteger requests = new AtomicInteger();
+    HttpServer server = startSchemaServer(requests);
+    try {
+      File including = writeIncludingSchema(tempDir, "including-plain.xsd", 
server);
+
+      
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(including);
+
+      assertEquals(1, requests.get());
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @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));
+  }
+
+  private HttpServer startSchemaServer(AtomicInteger requests) throws 
Exception {
+    HttpServer server =
+        HttpServer.create(new 
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+    server.createContext(
+        "/included.xsd",
+        exchange -> {
+          requests.incrementAndGet();
+          byte[] body = REMOTE_SCHEMA.getBytes(StandardCharsets.UTF_8);
+          exchange.getResponseHeaders().add("Content-Type", "text/xml");
+          exchange.sendResponseHeaders(200, body.length);
+          try (OutputStream out = exchange.getResponseBody()) {
+            out.write(body);
+          }
+        });
+    server.start();
+    return server;
+  }
+
+  private File writeIncludingSchema(Path tempDir, String filename, HttpServer 
server)
+      throws Exception {
+    File including = tempDir.resolve(filename).toFile();
+    Files.writeString(
+        including.toPath(),
+        "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>"
+            + "<xs:include schemaLocation=\"http://";
+            + server.getAddress().getHostString()
+            + ":"
+            + server.getAddress().getPort()
+            + "/included.xsd\"/>"
+            + "</xs:schema>");
+    return including;
+  }
+
+  private ErrorHandler collectingErrorHandler(List<SAXParseException> 
problems) {
+    return new ErrorHandler() {
+      @Override
+      public void warning(SAXParseException e) {
+        problems.add(e);
+      }
+
+      @Override
+      public void error(SAXParseException e) {
+        problems.add(e);
+      }
+
+      @Override
+      public void fatalError(SAXParseException e) throws SAXParseException {
+        throw e;
+      }
+    };
+  }
+}
diff --git 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMetaTest.java
 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMetaTest.java
index d0683e59ca..9f3c734864 100644
--- 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMetaTest.java
+++ 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorMetaTest.java
@@ -40,4 +40,9 @@ class XsdValidatorMetaTest {
     Assertions.assertEquals("filename", meta.getXsdSource());
     Assertions.assertTrue(meta.isAllowExternalEntities());
   }
+
+  @Test
+  void testExternalEntitiesAreDisabledByDefault() {
+    Assertions.assertFalse(new XsdValidatorMeta().isAllowExternalEntities());
+  }
 }

Reply via email to