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

ppkarwasz pushed a commit to branch feat/xinclude
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 16a75566bf95f6ca0389445571942f1e67b5ce7d
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Jun 11 10:50:20 2026 +0200

    Make XInclude opt-in and rework XML configuration hardening (#4064)
    
    XInclude support in XML configurations is now optional and disabled by
    default. It can be enabled with the `log4j2.configurationEnableXInclude`
    property, and failures of `setXIncludeAware` are no longer ignored since
    the user opted in for it. Fixes #4064.
    
    The XML factory hardening code is replaced with `copernik-xml-factory`,
    an incubating project for Apache Commons (or Xerces). The security
    features set on the factories are now tailored to the factory type and
    no longer swallowed, with Android support out-of-the-box.
    
    External entities and XInclude resources are resolved through a custom
    resolver based on `ConfigurationSource.fromUri`, so resolution now uses
    Log4j features (such as `classpath:` support and conventions on relative
    URIs) and honors the configured protocol restrictions.
    
    The `XmlConfigurationSecurity` test is renamed to
    `XmlConfigurationSecurityTest` so that Surefire runs it again; it was
    previously skipped because its name did not end in `Test`.
    
    Assisted-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../core/config/ConfigurationFactoryTest.java      |   2 +
 .../core/config/xml/XmlConfigurationSecurity.java  |  38 -------
 .../config/xml/XmlConfigurationSecurityTest.java   |  81 ++++++++++++++
 .../config/xml/XmlConfigurationXIncludeTest.java   |  78 +++++++++++++
 .../external-parameter-entity.xml}                 |   0
 .../xinclude.xml}                                  |   9 +-
 ...onSecurity.xml => log4j-xinclude-classpath.xml} |  23 ++--
 log4j-core/pom.xml                                 |   5 +
 .../log4j/core/config/xml/XmlConfiguration.java    | 122 ++++++++-------------
 .../log4j/osgi/tests/AbstractLoadBundleTest.java   |  36 +++---
 .../logging/log4j/osgi/tests/CoreOsgiTest.java     |   1 +
 .../logging/log4j/osgi/tests/DisruptorTest.java    |   1 +
 log4j-parent/pom.xml                               |   7 ++
 src/changelog/.2.x.x/4064_xinclude_opt_in.xml      |  12 ++
 src/changelog/.2.x.x/4064_xinclude_resolver.xml    |  11 ++
 15 files changed, 279 insertions(+), 147 deletions(-)

diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
index ee19ea7ea8..d6fa0d34ee 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
@@ -42,6 +42,7 @@ import org.apache.logging.log4j.core.appender.ConsoleAppender;
 import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
 import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
 import org.apache.logging.log4j.test.junit.TempLoggingDir;
 import org.apache.logging.log4j.util.Strings;
 import org.junit.jupiter.api.Tag;
@@ -103,6 +104,7 @@ class ConfigurationFactoryTest {
     }
 
     @Test
+    @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = 
"true")
     @LoggerContextSource("log4j-xinclude.xml")
     void xinclude(final LoggerContext context) throws IOException {
         checkConfiguration(context);
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurity.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurity.java
deleted file mode 100644
index 64aa3aa4d6..0000000000
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.logging.log4j.core.config.xml;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.config.Configurator;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-
-@Tag("functional")
-@Tag("security")
-class XmlConfigurationSecurity {
-
-    @Test
-    @Timeout(5)
-    void xmlSecurity() {
-        final LoggerContext context =
-                Configurator.initialize("XmlConfigurationSecurity", 
"XmlConfigurationSecurity.xml");
-        assertNotNull(context.getConfiguration().getAppender("list"));
-    }
-}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurityTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurityTest.java
new file mode 100644
index 0000000000..f9ae9e37bf
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurityTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.logging.log4j.core.config.xml;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationException;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Verifies the contract of {@link XmlConfiguration} regarding the resolution 
of external resources.
+ *
+ * <p>Configuration files are part of the trusted computing base (see the
+ * <a href="https://logging.apache.org/security.html";>Log4j security 
policy</a>); the hardenings checked here are
+ * defense in depth, not a security boundary against a malicious 
configuration. These tests only cover what Log4j is
+ * responsible for:</p>
+ * <ul>
+ *     <li>a forbidden external fetch is surfaced as a {@link 
ConfigurationException} (rather than a half-built
+ *         configuration), and</li>
+ *     <li>XInclude resources are subject to the same {@code 
ALLOWED_PROTOCOLS} restrictions as the configuration
+ *         file itself.</li>
+ * </ul>
+ *
+ * <p>All external fetches are forbidden; whether a forbidden fetch raises an 
error or is silently skipped is part of
+ * the contract of the {@code eu.copernik:copernik-xml-factory} library and is 
tested there.</p>
+ */
+@Tag("functional")
+@Tag("security")
+class XmlConfigurationSecurityTest {
+
+    private static final ConfigurationFactory FACTORY = new 
XmlConfigurationFactory();
+
+    private static Configuration getConfiguration(final String resource) {
+        return FACTORY.getConfiguration(null, null, 
URI.create("classpath:XmlConfigurationSecurity/" + resource));
+    }
+
+    /**
+     * A configuration that triggers a forbidden external fetch fails to load 
with a {@link ConfigurationException}.
+     * The {@code @Timeout} guards against an SSRF: a fetch attempt against 
the unreachable host would block until the
+     * connection times out.
+     */
+    @Test
+    @Timeout(5)
+    void forbiddenExternalFetchThrowsConfigurationException() {
+        assertThatThrownBy(() -> 
getConfiguration("external-parameter-entity.xml"))
+                .isInstanceOf(ConfigurationException.class);
+    }
+
+    /**
+     * XInclude resources are resolved through {@code ConfigurationSource}, so 
they are subject to the same
+     * {@code ALLOWED_PROTOCOLS} restrictions as the configuration file. With 
{@code http} excluded, an {@code http}
+     * include cannot be resolved and the configuration fails to load.
+     */
+    @Test
+    @Timeout(5)
+    @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = 
"true")
+    @SetTestProperty(key = "log4j2.configurationAllowedProtocols", value = 
"file")
+    void xIncludeRespectsAllowedProtocols() {
+        assertThatThrownBy(() -> 
getConfiguration("xinclude.xml")).isInstanceOf(ConfigurationException.class);
+    }
+}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationXIncludeTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationXIncludeTest.java
new file mode 100644
index 0000000000..b0c2d1c00e
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationXIncludeTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.logging.log4j.core.config.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URL;
+import javax.xml.parsers.DocumentBuilder;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+/**
+ * Unit tests for the XInclude handling of {@link 
XmlConfiguration#newDocumentBuilder(boolean)}.
+ * <p>
+ * These tests exercise the {@link DocumentBuilder} directly, so they are 
independent of the
+ * {@code log4j2.configurationEnableXInclude} property plumbing covered by the 
integration tests in
+ * {@code ConfigurationFactoryTest}.
+ * </p>
+ */
+class XmlConfigurationXIncludeTest {
+
+    private Document parse(final boolean enableXInclude) throws Exception {
+        return parse("/log4j-xinclude.xml", enableXInclude);
+    }
+
+    private Document parse(final String resource, final boolean 
enableXInclude) throws Exception {
+        final URL url = getClass().getResource(resource);
+        final DocumentBuilder builder = 
XmlConfiguration.newDocumentBuilder(enableXInclude);
+        final InputSource source = new InputSource(url.openStream());
+        // Required so that relative `href` attributes can be resolved against 
the configuration location.
+        source.setSystemId(url.toExternalForm());
+        return builder.parse(source);
+    }
+
+    @Test
+    void xInclude_enabled_resolves_includes() throws Exception {
+        final Document document = parse(true);
+        // The `xi:include` elements have been replaced by the content of the 
included files.
+        assertEquals(0, document.getElementsByTagNameNS("*", 
"include").getLength(), "no `xi:include` should remain");
+        assertEquals(1, document.getElementsByTagName("Console").getLength(), 
"Console appender from include");
+        assertEquals(1, document.getElementsByTagName("File").getLength(), 
"File appender from include");
+        assertEquals(1, document.getElementsByTagName("List").getLength(), 
"List appender from include");
+        assertEquals(2, document.getElementsByTagName("Logger").getLength(), 
"Loggers from include");
+    }
+
+    @Test
+    void xInclude_disabled_keeps_includes_unresolved() throws Exception {
+        final Document document = parse(false);
+        // Without XInclude support the `xi:include` elements are left 
untouched and nothing is included.
+        assertEquals(2, document.getElementsByTagNameNS("*", 
"include").getLength(), "`xi:include` elements remain");
+        assertEquals(0, document.getElementsByTagName("Console").getLength(), 
"no appenders should be included");
+    }
+
+    @Test
+    void xInclude_resolves_classpath_scheme() throws Exception {
+        // The custom resolver delegates to `ConfigurationSource`, which 
understands the `classpath:` URI scheme.
+        final Document document = parse("/log4j-xinclude-classpath.xml", true);
+        assertEquals(0, document.getElementsByTagNameNS("*", 
"include").getLength(), "no `xi:include` should remain");
+        assertEquals(
+                1, document.getElementsByTagName("Console").getLength(), 
"Console appender from classpath include");
+        assertEquals(2, document.getElementsByTagName("Logger").getLength(), 
"Loggers from classpath include");
+    }
+}
diff --git a/log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml 
b/log4j-core-test/src/test/resources/XmlConfigurationSecurity/external-parameter-entity.xml
similarity index 100%
copy from log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml
copy to 
log4j-core-test/src/test/resources/XmlConfigurationSecurity/external-parameter-entity.xml
diff --git a/log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml 
b/log4j-core-test/src/test/resources/XmlConfigurationSecurity/xinclude.xml
similarity index 82%
copy from log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml
copy to log4j-core-test/src/test/resources/XmlConfigurationSecurity/xinclude.xml
index 4a648ad5e5..98b2b76128 100644
--- a/log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml
+++ b/log4j-core-test/src/test/resources/XmlConfigurationSecurity/xinclude.xml
@@ -15,16 +15,15 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<!DOCTYPE Configuration [
- <!ENTITY % dtd SYSTEM "http://localhost:12345/evil1.dtd";>
-%dtd;]>
-<Configuration status="OFF" name="XmlConfigurationSecurity">
+<Configuration xmlns:xi="http://www.w3.org/2001/XInclude";
+  status="OFF" name="XmlConfigurationSecurity">
   <Appenders>
     <List name="list">
       <PatternLayout pattern="%m%n"/>
     </List>
   </Appenders>
-
+  <!-- An `http` resource that `ALLOWED_PROTOCOLS` must reject. -->
+  <xi:include href="http://localhost:12345/evil3.xml"/>
   <Loggers>
     <Root level="info">
       <AppenderRef ref="list"/>
diff --git a/log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml 
b/log4j-core-test/src/test/resources/log4j-xinclude-classpath.xml
similarity index 70%
rename from log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml
rename to log4j-core-test/src/test/resources/log4j-xinclude-classpath.xml
index 4a648ad5e5..e9e9802fc9 100644
--- a/log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml
+++ b/log4j-core-test/src/test/resources/log4j-xinclude-classpath.xml
@@ -15,19 +15,12 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<!DOCTYPE Configuration [
- <!ENTITY % dtd SYSTEM "http://localhost:12345/evil1.dtd";>
-%dtd;]>
-<Configuration status="OFF" name="XmlConfigurationSecurity">
-  <Appenders>
-    <List name="list">
-      <PatternLayout pattern="%m%n"/>
-    </List>
-  </Appenders>
-
-  <Loggers>
-    <Root level="info">
-      <AppenderRef ref="list"/>
-    </Root>
-  </Loggers>
+<Configuration xmlns:xi="http://www.w3.org/2001/XInclude";
+  status="OFF" name="XMLConfigTest">
+  <Properties>
+    <Property name="filename">${test:logging.path}/test-xinclude.log</Property>
+  </Properties>
+  <ThresholdFilter level="debug"/>
+  <xi:include href="classpath:log4j-xinclude-appenders.xml" />
+  <xi:include href="classpath:log4j-xinclude-loggers.xml" />
 </Configuration>
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index b2f34d4ab9..4edf0860c4 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -157,6 +157,11 @@
       <artifactId>commons-csv</artifactId>
       <optional>true</optional>
     </dependency>
+    <!-- Create hardened JAXP factories -->
+    <dependency>
+      <groupId>eu.copernik</groupId>
+      <artifactId>copernik-xml-factory</artifactId>
+    </dependency>
     <!-- Alternative implementation of BlockingQueue using Conversant 
Disruptor for AsyncAppender -->
     <dependency>
       <groupId>com.conversantmedia</groupId>
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
index 3218589bb1..56f62ee58a 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
@@ -16,9 +16,12 @@
  */
 package org.apache.logging.log4j.core.config.xml;
 
+import eu.copernik.xml.factory.XmlFactories;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -35,6 +38,7 @@ import javax.xml.validation.Validator;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.AbstractConfiguration;
 import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationException;
 import org.apache.logging.log4j.core.config.ConfigurationSource;
 import org.apache.logging.log4j.core.config.Node;
 import org.apache.logging.log4j.core.config.Reconfigurable;
@@ -45,13 +49,14 @@ import org.apache.logging.log4j.core.util.Closer;
 import org.apache.logging.log4j.core.util.Integers;
 import org.apache.logging.log4j.core.util.Loader;
 import org.apache.logging.log4j.core.util.Patterns;
-import org.apache.logging.log4j.core.util.Throwables;
+import org.apache.logging.log4j.util.PropertiesUtil;
 import org.w3c.dom.Attr;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.NodeList;
 import org.w3c.dom.Text;
+import org.xml.sax.EntityResolver;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
@@ -60,8 +65,7 @@ import org.xml.sax.SAXException;
  */
 public class XmlConfiguration extends AbstractConfiguration implements 
Reconfigurable {
 
-    private static final String XINCLUDE_FIXUP_LANGUAGE = 
"http://apache.org/xml/features/xinclude/fixup-language";;
-    private static final String XINCLUDE_FIXUP_BASE_URIS = 
"http://apache.org/xml/features/xinclude/fixup-base-uris";;
+    private static final String ENABLE_XINCLUDE_PROP = 
"log4j2.configurationEnableXInclude";
 
     private final List<Status> status = new ArrayList<>();
     private Element rootElement;
@@ -84,24 +88,9 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
             }
             final InputSource source = new InputSource(new 
ByteArrayInputStream(buffer));
             source.setSystemId(configSource.getLocation());
-            final DocumentBuilder documentBuilder = newDocumentBuilder(true);
-            Document document;
-            try {
-                document = documentBuilder.parse(source);
-            } catch (final Exception e) {
-                // LOG4J2-1127
-                final Throwable throwable = Throwables.getRootCause(e);
-                if (throwable instanceof UnsupportedOperationException) {
-                    LOGGER.warn(
-                            "The DocumentBuilder {} does not support an 
operation: {}."
-                                    + "Trying again without XInclude...",
-                            documentBuilder,
-                            e);
-                    document = newDocumentBuilder(false).parse(source);
-                } else {
-                    throw e;
-                }
-            }
+            final boolean xIncludeAware = 
PropertiesUtil.getProperties().getBooleanProperty(ENABLE_XINCLUDE_PROP);
+            final DocumentBuilder documentBuilder = 
newDocumentBuilder(xIncludeAware);
+            final Document document = documentBuilder.parse(source);
             rootElement = document.getDocumentElement();
             final Map<String, String> attrs = processAttributes(rootNode, 
rootElement);
             final StatusConfiguration statusConfig = new 
StatusConfiguration().withStatus(getDefaultStatus());
@@ -135,6 +124,7 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
             statusConfig.initialize();
         } catch (final SAXException | IOException | 
ParserConfigurationException e) {
             LOGGER.error("Error parsing " + configSource.getLocation(), e);
+            throw new ConfigurationException("Error parsing " + 
configSource.getLocation(), e);
         }
         if (strict && schemaResource != null && buffer != null) {
             try (final InputStream is =
@@ -172,67 +162,22 @@ public class XmlConfiguration extends 
AbstractConfiguration implements Reconfigu
     /**
      * Creates a new DocumentBuilder suitable for parsing a configuration file.
      *
-     * @param xIncludeAware enabled XInclude
+     * @param enableXInclude enabled XInclude
      * @return a new DocumentBuilder
      * @throws ParserConfigurationException if a DocumentBuilder cannot be 
created, which satisfies the configuration requested.
      */
-    static DocumentBuilder newDocumentBuilder(final boolean xIncludeAware) 
throws ParserConfigurationException {
-        final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+    static DocumentBuilder newDocumentBuilder(final boolean enableXInclude) 
throws ParserConfigurationException {
+        final DocumentBuilderFactory factory = 
XmlFactories.newDocumentBuilderFactory();
         factory.setNamespaceAware(true);
-
-        disableDtdProcessing(factory);
-
-        if (xIncludeAware) {
-            enableXInclude(factory);
-        }
-        return factory.newDocumentBuilder();
-    }
-
-    private static void disableDtdProcessing(final DocumentBuilderFactory 
factory) {
-        factory.setValidating(false);
-        factory.setExpandEntityReferences(false);
-        setFeature(factory, 
"http://xml.org/sax/features/external-general-entities";, false);
-        setFeature(factory, 
"http://xml.org/sax/features/external-parameter-entities";, false);
-        setFeature(factory, 
"http://apache.org/xml/features/nonvalidating/load-external-dtd";, false);
-    }
-
-    private static void setFeature(
-            final DocumentBuilderFactory factory, final String featureName, 
final boolean value) {
-        try {
-            factory.setFeature(featureName, value);
-        } catch (final ParserConfigurationException e) {
-            LOGGER.warn(
-                    "The DocumentBuilderFactory [{}] does not support the 
feature [{}]: {}", factory, featureName, e);
-        } catch (final AbstractMethodError err) {
-            LOGGER.warn(
-                    "The DocumentBuilderFactory [{}] is out of date and does 
not support setFeature: {}", factory, err);
+        if (enableXInclude) {
+            factory.setXIncludeAware(true);
         }
-    }
 
-    /**
-     * Enables XInclude for the given DocumentBuilderFactory
-     *
-     * @param factory a DocumentBuilderFactory
-     */
-    private static void enableXInclude(final DocumentBuilderFactory factory) {
-        try {
-            factory.setXIncludeAware(true);
-            // LOG4J2-3531: Xerces only checks if the feature is supported 
when creating a factory. To reproduce:
-            // 
-Dorg.apache.xerces.xni.parser.XMLParserConfiguration=org.apache.xerces.parsers.XML11NonValidatingConfiguration
-            try {
-                factory.newDocumentBuilder();
-            } catch (final ParserConfigurationException e) {
-                factory.setXIncludeAware(false);
-                LOGGER.warn("The DocumentBuilderFactory [{}] does not support 
XInclude: {}", factory, e);
-            }
-        } catch (final UnsupportedOperationException e) {
-            LOGGER.warn("The DocumentBuilderFactory [{}] does not support 
XInclude: {}", factory, e);
-        } catch (final AbstractMethodError | NoSuchMethodError err) {
-            LOGGER.warn(
-                    "The DocumentBuilderFactory [{}] is out of date and does 
not support XInclude: {}", factory, err);
+        final DocumentBuilder builder = factory.newDocumentBuilder();
+        if (enableXInclude) {
+            
builder.setEntityResolver(ConfigurationSourceEntityResolver.INSTANCE);
         }
-        setFeature(factory, XINCLUDE_FIXUP_BASE_URIS, true);
-        setFeature(factory, XINCLUDE_FIXUP_LANGUAGE, true);
+        return builder;
     }
 
     @Override
@@ -368,4 +313,31 @@ public class XmlConfiguration extends 
AbstractConfiguration implements Reconfigu
             return "Status [name=" + name + ", element=" + element + ", 
errorType=" + errorType + "]";
         }
     }
+
+    /**
+     * Entity resolver that resolves external entities the same way the 
configuration itself is resolved.
+     */
+    private static class ConfigurationSourceEntityResolver implements 
EntityResolver {
+
+        private static final EntityResolver INSTANCE = new 
ConfigurationSourceEntityResolver();
+
+        @Override
+        public InputSource resolveEntity(final String publicId, final String 
systemId) throws SAXException {
+            InputSource source = null;
+            try {
+                final ConfigurationSource configurationSource = 
ConfigurationSource.fromUri(new URI(systemId));
+                if (configurationSource != null) {
+                    source = new 
InputSource(configurationSource.getInputStream());
+                    source.setPublicId(publicId);
+                    source.setSystemId(systemId);
+                }
+            } catch (final URISyntaxException e) {
+                throw new SAXException("Unable to resolve system id " + 
systemId, e);
+            }
+            if (source == null) {
+                throw new SAXException("Unable to resolve entity: " + 
systemId);
+            }
+            return source;
+        }
+    }
 }
diff --git 
a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java
 
b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java
index 06bb63611d..9d370ccf5b 100644
--- 
a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java
+++ 
b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java
@@ -75,6 +75,10 @@ abstract class AbstractLoadBundleTest {
         return installBundle("org.apache.logging.log4j.api.test");
     }
 
+    private Bundle getXmlFactoryBundle() throws BundleException {
+        return installBundle("eu.copernik.xml.factory");
+    }
+
     /**
      * Tests starting, then stopping, then restarting, then stopping, and 
finally uninstalling the API and Core bundles
      */
@@ -82,19 +86,20 @@ abstract class AbstractLoadBundleTest {
     public void testApiCoreStartStopStartStop() throws BundleException {
 
         final Bundle api = getApiBundle();
+        final Bundle xmlFactory = getXmlFactoryBundle();
         final Bundle core = getCoreBundle();
         assertEquals(Bundle.INSTALLED, api.getState(), "api is not in 
INSTALLED state");
         assertEquals(Bundle.INSTALLED, core.getState(), "core is not in 
INSTALLED state");
 
         // 1st start-stop
-        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core);
-        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api);
+        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, 
xmlFactory, core);
+        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, 
xmlFactory, api);
 
         // 2nd start-stop
-        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core);
-        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api);
+        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, 
xmlFactory, core);
+        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, 
xmlFactory, api);
 
-        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, 
api);
+        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, 
xmlFactory, api);
     }
 
     /**
@@ -104,9 +109,10 @@ abstract class AbstractLoadBundleTest {
     public void testClassNotFoundErrorLogger() throws BundleException {
 
         final Bundle api = getApiBundle();
+        final Bundle xmlFactory = getXmlFactoryBundle();
         final Bundle core = getCoreBundle();
 
-        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api);
+        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, 
xmlFactory);
         // fails if LOG4J2-1637 is not fixed
         try {
             core.start();
@@ -126,8 +132,8 @@ abstract class AbstractLoadBundleTest {
         }
         assertEquals(Bundle.ACTIVE, core.getState(), String.format("`%s` 
bundle state mismatch", core));
 
-        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api);
-        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, 
api);
+        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, 
xmlFactory, api);
+        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, 
xmlFactory, api);
     }
 
     /**
@@ -138,10 +144,11 @@ abstract class AbstractLoadBundleTest {
     public void testLog4J12Fragement() throws BundleException, 
ReflectiveOperationException {
 
         final Bundle api = getApiBundle();
+        final Bundle xmlFactory = getXmlFactoryBundle();
         final Bundle core = getCoreBundle();
         final Bundle compat = get12ApiBundle();
 
-        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core);
+        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, 
xmlFactory, core);
 
         final Class<?> coreClassFromCore = 
core.loadClass("org.apache.logging.log4j.core.Core");
         final Class<?> levelClassFrom12API = 
core.loadClass("org.apache.log4j.Level");
@@ -156,8 +163,8 @@ abstract class AbstractLoadBundleTest {
                 levelClassFromAPI.getClassLoader(),
                 "expected 1.2 API Level NOT to have the same class loader as 
API Level");
 
-        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api);
-        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, 
compat, core, api);
+        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, 
xmlFactory, api);
+        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, 
compat, core, xmlFactory, api);
     }
 
     /**
@@ -166,13 +173,14 @@ abstract class AbstractLoadBundleTest {
     @Test
     public void testServiceLoader() throws BundleException, 
ReflectiveOperationException {
         final Bundle api = getApiBundle();
+        final Bundle xmlFactory = getXmlFactoryBundle();
         final Bundle core = getCoreBundle();
         final Bundle apiTests = getApiTestsBundle();
 
         final Class<?> osgiServiceLocator = 
api.loadClass("org.apache.logging.log4j.util.OsgiServiceLocator");
         assertTrue((boolean) 
osgiServiceLocator.getMethod("isAvailable").invoke(null), "OsgiServiceLocator 
is active");
 
-        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core, 
apiTests);
+        doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, 
xmlFactory, core, apiTests);
 
         final Class<?> osgiServiceLocatorTest =
                 
apiTests.loadClass("org.apache.logging.log4j.test.util.OsgiServiceLocatorTest");
@@ -187,8 +195,8 @@ abstract class AbstractLoadBundleTest {
                 "org.apache.logging.log4j.core.impl.Log4jProvider",
                 services.get(0).getClass().getName());
 
-        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, apiTests, 
core, api);
-        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, 
apiTests, core, api);
+        doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, apiTests, 
core, xmlFactory, api);
+        doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, 
apiTests, core, xmlFactory, api);
     }
 
     private static void doOnBundlesAndVerifyState(
diff --git 
a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java
 
b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java
index 493bb61c9e..77d7f54cdc 100644
--- 
a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java
+++ 
b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java
@@ -46,6 +46,7 @@ public class CoreOsgiTest {
     public Option[] config() {
         return options(
                 linkBundle("org.apache.logging.log4j.api"),
+                linkBundle("eu.copernik.xml.factory"),
                 linkBundle("org.apache.logging.log4j.core"),
                 linkBundle("org.apache.logging.log4j.1.2.api").start(false),
                 // required by Pax Exam's logging
diff --git 
a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java
 
b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java
index 813331210c..018edca9f5 100644
--- 
a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java
+++ 
b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java
@@ -52,6 +52,7 @@ public class DisruptorTest {
     public Option[] config() {
         return options(
                 linkBundle("org.apache.logging.log4j.api"),
+                linkBundle("eu.copernik.xml.factory"),
                 linkBundle("org.apache.logging.log4j.core"),
                 linkBundle("com.lmax.disruptor"),
                 // required by Pax Exam's logging
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 9a7b5aa2c8..869fab6c90 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -78,6 +78,7 @@
     <commons-logging.version>1.3.5</commons-logging.version>
     <!-- `com.conversantmedia:disruptor` version 1.2.16 requires Java 9: -->
     <conversant.disruptor.version>1.2.15</conversant.disruptor.version>
+    <copernik-xml-factory.version>0.1.1</copernik-xml-factory.version>
     <disruptor.version>3.4.4</disruptor.version>
     <embedded-ldap.version>0.9.0</embedded-ldap.version>
     <error-prone-annotations.version>2.38.0</error-prone-annotations.version>
@@ -392,6 +393,12 @@
         <version>${commons-pool2.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>eu.copernik</groupId>
+        <artifactId>copernik-xml-factory</artifactId>
+        <version>${copernik-xml-factory.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>com.conversantmedia</groupId>
         <artifactId>disruptor</artifactId>
diff --git a/src/changelog/.2.x.x/4064_xinclude_opt_in.xml 
b/src/changelog/.2.x.x/4064_xinclude_opt_in.xml
new file mode 100644
index 0000000000..24a23488c6
--- /dev/null
+++ b/src/changelog/.2.x.x/4064_xinclude_opt_in.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="changed">
+  <issue id="4064" 
link="https://github.com/apache/logging-log4j2/issues/4064"/>
+  <description format="asciidoc">
+    XInclude support in XML configurations is now opt-in (disabled by default) 
and can be enabled with the `log4j2.configurationEnableXInclude` property.
+  </description>
+</entry>
diff --git a/src/changelog/.2.x.x/4064_xinclude_resolver.xml 
b/src/changelog/.2.x.x/4064_xinclude_resolver.xml
new file mode 100644
index 0000000000..89ce4d936e
--- /dev/null
+++ b/src/changelog/.2.x.x/4064_xinclude_resolver.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="added">
+  <description format="asciidoc">
+    XInclude resources are now resolved through `ConfigurationSource`, so they 
support the Log4j URI conventions.
+  </description>
+</entry>


Reply via email to