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

ppkarwasz pushed a commit to branch feat/use-commons-xml
in repository https://gitbox.apache.org/repos/asf/commons-configuration.git

commit 47425633db08ac9c268c9700f5aeaca760daab96
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sun Aug 30 22:25:52 2026 +0200

    Harden XML parsing via commons-xml
    
    Replace all direct JAXP factory instantiations (DocumentBuilderFactory,
    SAXParserFactory, TransformerFactory) with the secure factories from
    org.apache.commons:commons-xml. These factories enable
    FEATURE_SECURE_PROCESSING and install a non-removable entity-resolver
    floor on every parser they produce: external DTD, entity, schema and
    XInclude lookups that a caller-set resolver does not resolve are
    resolved to empty content instead of being fetched, and internal entity
    expansion is bounded, regardless of the JAXP implementation on the
    classpath.
    
    Hardening the parsing of a configuration file is admittedly not
    necessary: configuration files are normally trusted. This limits the
    side-effects if a user (against advice) decides to parse untrusted
    configuration files.
    
    Changes:
    - Add the commons-xml dependency (1.0.0-SNAPSHOT until its first
      release).
    - Route factory creation through SecureDocumentBuilderFactory,
      SecureSAXParserFactory and SecureTransformerFactory in
      XMLConfiguration, XMLDocumentHelper, XMLPropertiesConfiguration and
      XMLPropertyListConfiguration, plus the affected tests.
    - No explicit hardening of the source passed to
      XMLDocumentHelper.transform is needed: transformers created by
      SecureTransformerFactory rewrite their sources on every transform
      call.
    - XMLConfiguration keeps its DefaultEntityResolver contract (return
      null for unknown entities): a null return no longer lets the parser
      fetch the external resource, because the resolver floor resolves it
      to empty content instead.
    - Parse with EntityResolver2 handling disabled
      (http://xml.org/sax/features/use-entity-resolver2) when schema
      validation is enabled: the JDK does not mark schema documents
      supplied by an EntityResolver2 as resolver-created, so the
      accessExternalSchema check enabled by secure processing refuses them
      even when a caller-set resolver (such as CatalogResolver) resolves
      them locally. The plain EntityResolver path marks them correctly and
      keeps resolver-based schema validation working.
    - Run the CI build with -Puse-apache-snapshots (inherited from the
      org.apache:apache parent POM) so the commons-xml SNAPSHOT resolves.
    
    Assisted-By: Claude Opus 4.8 (1M context) <[email protected]>
    Assisted-By: Claude Fable 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01NS6CoaDG2mfSNpy4Ukhvrn
---
 .github/workflows/maven.yml                               |  2 +-
 pom.xml                                                   |  5 +++++
 .../apache/commons/configuration2/XMLConfiguration.java   | 11 ++++++++++-
 .../apache/commons/configuration2/XMLDocumentHelper.java  | 15 ++++-----------
 .../configuration2/XMLPropertiesConfiguration.java        |  3 ++-
 .../plist/XMLPropertyListConfiguration.java               |  3 ++-
 .../configuration2/TestBaseConfigurationXMLReader.java    |  4 ++--
 .../TestHierarchicalConfigurationXMLReader.java           |  4 ++--
 .../commons/configuration2/TestXMLConfiguration.java      | 10 ++++++----
 .../commons/configuration2/TestXMLDocumentHelper.java     |  3 ++-
 .../configuration2/TestXMLPropertiesConfiguration.java    |  8 +++++---
 11 files changed, 41 insertions(+), 27 deletions(-)

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 61791213a..99cebcac0 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -49,6 +49,6 @@ jobs:
         java-version: ${{ matrix.java }}
         cache: 'maven'
     - name: Build with Maven
-      run: mvn --errors --show-version --batch-mode --no-transfer-progress
+      run: mvn --errors --show-version --batch-mode --no-transfer-progress 
-Puse-apache-snapshots
 
 # For Java 11, you can be more strict: 
-DadditionalJOption=-Xdoclint/package:-org.apache.commons.configuration2.plist
diff --git a/pom.xml b/pom.xml
index 190a769ff..320159ba7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -96,6 +96,11 @@
     </site>
   </distributionManagement>
   <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-xml</artifactId>
+      <version>1.0.0-SNAPSHOT</version>
+    </dependency>
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
diff --git 
a/src/main/java/org/apache/commons/configuration2/XMLConfiguration.java 
b/src/main/java/org/apache/commons/configuration2/XMLConfiguration.java
index fb97d08f3..eca89a537 100644
--- a/src/main/java/org/apache/commons/configuration2/XMLConfiguration.java
+++ b/src/main/java/org/apache/commons/configuration2/XMLConfiguration.java
@@ -53,6 +53,7 @@ import org.apache.commons.configuration2.tree.NodeTreeWalker;
 import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.mutable.MutableObject;
+import org.apache.commons.xml.SecureDocumentBuilderFactory;
 import org.w3c.dom.Attr;
 import org.w3c.dom.CDATASection;
 import org.w3c.dom.Document;
@@ -693,12 +694,20 @@ public class XMLConfiguration extends 
BaseHierarchicalConfiguration implements F
         if (getDocumentBuilder() != null) {
             return getDocumentBuilder();
         }
-        final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+        final DocumentBuilderFactory factory = 
SecureDocumentBuilderFactory.newInstance();
         if (isValidating()) {
             factory.setValidating(true);
             if (isSchemaValidation()) {
                 factory.setNamespaceAware(true);
                 factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
+                try {
+                    // Due to a bug, the JDK fails to mark schema documents 
supplied by an EntityResolver2 as resolver-created,
+                    // so the accessExternalSchema check is applied to the 
resolved source and denies access.
+                    // Downgrading the resolver to a plain EntityResolver 
works around the check.
+                    
factory.setFeature("http://xml.org/sax/features/use-entity-resolver2";, false);
+                } catch (final ParserConfigurationException e) {
+                    // SAX-specific feature: parsers that do not recognize it 
keep EntityResolver2 handling.
+                }
             }
         }
 
diff --git 
a/src/main/java/org/apache/commons/configuration2/XMLDocumentHelper.java 
b/src/main/java/org/apache/commons/configuration2/XMLDocumentHelper.java
index 3c570e086..62d2ec966 100644
--- a/src/main/java/org/apache/commons/configuration2/XMLDocumentHelper.java
+++ b/src/main/java/org/apache/commons/configuration2/XMLDocumentHelper.java
@@ -33,6 +33,8 @@ import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.dom.DOMSource;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.xml.SecureDocumentBuilderFactory;
+import org.apache.commons.xml.SecureTransformerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -92,15 +94,6 @@ final class XMLDocumentHelper {
         }
     }
 
-    /**
-     * Creates a new {@code DocumentBuilderFactory} instance.
-     *
-     * @return The new factory object
-     */
-    private static DocumentBuilderFactory createDocumentBuilderFactory() {
-        return DocumentBuilderFactory.newInstance();
-    }
-
     /**
      * Creates the element mapping for the specified documents. For each node 
in the source document an entry is created
      * pointing to the corresponding node in the destination object.
@@ -163,7 +156,7 @@ final class XMLDocumentHelper {
      * @return The {@code TransformerFactory}
      */
     static TransformerFactory createTransformerFactory() {
-        return TransformerFactory.newInstance();
+        return SecureTransformerFactory.newInstance();
     }
 
     /**
@@ -184,7 +177,7 @@ final class XMLDocumentHelper {
      * @throws ConfigurationException if an error occurs when creating the 
document
      */
     public static XMLDocumentHelper forNewDocument(final String 
rootElementName) throws ConfigurationException {
-        final Document doc = 
createDocumentBuilder(createDocumentBuilderFactory()).newDocument();
+        final Document doc = 
createDocumentBuilder(SecureDocumentBuilderFactory.newInstance()).newDocument();
         final Element rootElem = doc.createElement(rootElementName);
         doc.appendChild(rootElem);
         return new XMLDocumentHelper(doc, emptyElementMapping(), null, null);
diff --git 
a/src/main/java/org/apache/commons/configuration2/XMLPropertiesConfiguration.java
 
b/src/main/java/org/apache/commons/configuration2/XMLPropertiesConfiguration.java
index d5b217e63..8ba9696bd 100644
--- 
a/src/main/java/org/apache/commons/configuration2/XMLPropertiesConfiguration.java
+++ 
b/src/main/java/org/apache/commons/configuration2/XMLPropertiesConfiguration.java
@@ -32,6 +32,7 @@ import 
org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.io.FileLocator;
 import org.apache.commons.configuration2.io.FileLocatorAware;
 import org.apache.commons.text.StringEscapeUtils;
+import org.apache.commons.xml.SecureSAXParserFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -221,7 +222,7 @@ public class XMLPropertiesConfiguration extends 
BaseConfiguration implements Fil
 
     @Override
     public void read(final Reader in) throws ConfigurationException {
-        final SAXParserFactory factory = SAXParserFactory.newInstance();
+        final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
         factory.setNamespaceAware(false);
         factory.setValidating(true);
         try {
diff --git 
a/src/main/java/org/apache/commons/configuration2/plist/XMLPropertyListConfiguration.java
 
b/src/main/java/org/apache/commons/configuration2/plist/XMLPropertyListConfiguration.java
index 04b415cca..0fb394d1c 100644
--- 
a/src/main/java/org/apache/commons/configuration2/plist/XMLPropertyListConfiguration.java
+++ 
b/src/main/java/org/apache/commons/configuration2/plist/XMLPropertyListConfiguration.java
@@ -55,6 +55,7 @@ import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.commons.configuration2.tree.InMemoryNodeModel;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.StringEscapeUtils;
+import org.apache.commons.xml.SecureSAXParserFactory;
 import org.xml.sax.Attributes;
 import org.xml.sax.EntityResolver;
 import org.xml.sax.InputSource;
@@ -661,7 +662,7 @@ public class XMLPropertyListConfiguration extends 
BaseHierarchicalConfiguration
         // parse the file
         final XMLPropertyListHandler handler = new XMLPropertyListHandler();
         try {
-            final SAXParserFactory factory = SAXParserFactory.newInstance();
+            final SAXParserFactory factory = 
SecureSAXParserFactory.newInstance();
             factory.setValidating(true);
             final XMLReader xmlReader = factory.newSAXParser().getXMLReader();
             xmlReader.setEntityResolver(resolver);
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestBaseConfigurationXMLReader.java
 
b/src/test/java/org/apache/commons/configuration2/TestBaseConfigurationXMLReader.java
index 6ce26a9a9..01c32ac43 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestBaseConfigurationXMLReader.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestBaseConfigurationXMLReader.java
@@ -27,11 +27,11 @@ import java.util.Arrays;
 import java.util.Iterator;
 
 import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.sax.SAXSource;
 
 import org.apache.commons.jxpath.JXPathContext;
+import org.apache.commons.xml.SecureTransformerFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
@@ -80,7 +80,7 @@ public class TestBaseConfigurationXMLReader {
     private void checkDocument(final BaseConfigurationXMLReader creader, final 
String rootName) throws Exception {
         final SAXSource source = new SAXSource(creader, new InputSource());
         final DOMResult result = new DOMResult();
-        final Transformer trans = 
TransformerFactory.newInstance().newTransformer();
+        final Transformer trans = 
SecureTransformerFactory.newInstance().newTransformer();
         trans.transform(source, result);
         final Node root = ((Document) result.getNode()).getDocumentElement();
         final JXPathContext ctx = JXPathContext.newContext(root);
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestHierarchicalConfigurationXMLReader.java
 
b/src/test/java/org/apache/commons/configuration2/TestHierarchicalConfigurationXMLReader.java
index ad801f7a8..875f44604 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestHierarchicalConfigurationXMLReader.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestHierarchicalConfigurationXMLReader.java
@@ -20,13 +20,13 @@ package org.apache.commons.configuration2;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.sax.SAXSource;
 
 import org.apache.commons.configuration2.io.FileHandler;
 import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.commons.jxpath.JXPathContext;
+import org.apache.commons.xml.SecureTransformerFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
@@ -53,7 +53,7 @@ public class TestHierarchicalConfigurationXMLReader {
     void testParse() throws Exception {
         final SAXSource source = new SAXSource(parser, new InputSource());
         final DOMResult result = new DOMResult();
-        final Transformer trans = 
TransformerFactory.newInstance().newTransformer();
+        final Transformer trans = 
SecureTransformerFactory.newInstance().newTransformer();
         trans.transform(source, result);
         final Node root = ((Document) result.getNode()).getDocumentElement();
         final JXPathContext ctx = JXPathContext.newContext(root);
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestXMLConfiguration.java 
b/src/test/java/org/apache/commons/configuration2/TestXMLConfiguration.java
index 279216662..c3fb77ed2 100644
--- a/src/test/java/org/apache/commons/configuration2/TestXMLConfiguration.java
+++ b/src/test/java/org/apache/commons/configuration2/TestXMLConfiguration.java
@@ -64,6 +64,8 @@ import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.commons.configuration2.tree.NodeStructureHelper;
 import org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.xml.SecureDocumentBuilderFactory;
+import org.apache.commons.xml.SecureTransformerFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -155,7 +157,7 @@ public class TestXMLConfiguration {
         final Source source = new DOMSource(node);
         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
         final Result result = new StreamResult(bos);
-        final TransformerFactory factory = TransformerFactory.newInstance();
+        final TransformerFactory factory = 
SecureTransformerFactory.newInstance();
         factory.newTransformer().transform(source, result);
         // 4. Return the resulting byte array
         return bos.toByteArray();
@@ -184,7 +186,7 @@ public class TestXMLConfiguration {
 
     private Node buildDomNodeFixture() throws SAXException, IOException, 
ParserConfigurationException {
         final String content = "<configuration><test 
attr=\"x\">1</test></configuration>";
-        final Node document = 
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new 
ByteArrayInputStream(content.getBytes()));
+        final Node document = 
SecureDocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new 
ByteArrayInputStream(content.getBytes()));
         final Node node = document.getFirstChild().getFirstChild(); // <test>
         assertEquals("test", node.getNodeName()); // sanity check
         return node;
@@ -239,7 +241,7 @@ public class TestXMLConfiguration {
      * @throws ParserConfigurationException if an error occurs
      */
     private DocumentBuilder createValidatingDocBuilder() throws 
ParserConfigurationException {
-        final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+        final DocumentBuilderFactory factory = 
SecureDocumentBuilderFactory.newInstance();
         factory.setValidating(true);
         final DocumentBuilder builder = factory.newDocumentBuilder();
         builder.setErrorHandler(new DefaultHandler() {
@@ -252,7 +254,7 @@ public class TestXMLConfiguration {
     }
 
     private Document parseXml(final String xml) throws SAXException, 
IOException, ParserConfigurationException {
-        return 
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+        return 
SecureDocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestXMLDocumentHelper.java 
b/src/test/java/org/apache/commons/configuration2/TestXMLDocumentHelper.java
index 35cff0a22..50b6f3298 100644
--- a/src/test/java/org/apache/commons/configuration2/TestXMLDocumentHelper.java
+++ b/src/test/java/org/apache/commons/configuration2/TestXMLDocumentHelper.java
@@ -45,6 +45,7 @@ import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.xml.SecureDocumentBuilderFactory;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -134,7 +135,7 @@ public class TestXMLDocumentHelper {
      * @return The parsed document
      */
     private static Document loadDocument(final String name) throws 
IOException, SAXException, ParserConfigurationException {
-        final DocumentBuilder builder = 
DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        final DocumentBuilder builder = 
SecureDocumentBuilderFactory.newInstance().newDocumentBuilder();
         return builder.parse(ConfigurationAssert.getTestFile(name));
     }
 
diff --git 
a/src/test/java/org/apache/commons/configuration2/TestXMLPropertiesConfiguration.java
 
b/src/test/java/org/apache/commons/configuration2/TestXMLPropertiesConfiguration.java
index b7e83751d..8304161c0 100644
--- 
a/src/test/java/org/apache/commons/configuration2/TestXMLPropertiesConfiguration.java
+++ 
b/src/test/java/org/apache/commons/configuration2/TestXMLPropertiesConfiguration.java
@@ -35,6 +35,8 @@ import javax.xml.transform.stream.StreamResult;
 
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.io.FileHandler;
+import org.apache.commons.xml.SecureDocumentBuilderFactory;
+import org.apache.commons.xml.SecureTransformerFactory;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.w3c.dom.Document;
@@ -72,7 +74,7 @@ public class TestXMLPropertiesConfiguration {
         assertThrows(NullPointerException.class, () -> new 
XMLPropertiesConfiguration(null));
         // Normal case
         final URL location = 
ConfigurationAssert.getTestURL(TEST_PROPERTIES_FILE);
-        final DocumentBuilderFactory dbFactory = 
DocumentBuilderFactory.newInstance();
+        final DocumentBuilderFactory dbFactory = 
SecureDocumentBuilderFactory.newInstance();
         final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
         dBuilder.setEntityResolver((publicId, systemId) -> new 
InputSource(getClass().getClassLoader().getResourceAsStream("properties.dtd")));
         final File file = new File(location.toURI());
@@ -101,11 +103,11 @@ public class TestXMLPropertiesConfiguration {
         final File saveFile = newFile("test2.properties.xml", tempFolder);
 
         // save as DOM into saveFile
-        final DocumentBuilderFactory dbFactory = 
DocumentBuilderFactory.newInstance();
+        final DocumentBuilderFactory dbFactory = 
SecureDocumentBuilderFactory.newInstance();
         final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
         final Document document = dBuilder.newDocument();
         conf.save(document, document);
-        final TransformerFactory tFactory = TransformerFactory.newInstance();
+        final TransformerFactory tFactory = 
SecureTransformerFactory.newInstance();
         final Transformer transformer = tFactory.newTransformer();
         final DOMSource source = new DOMSource(document);
         final Result result = new StreamResult(saveFile);

Reply via email to