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

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

commit ab09d5f22c54dc0b2fd668ab6ab938dd5062caf5
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Fri Jun 26 14:34:44 2026 +0200

    Make XInclude processing in XML configurations opt-in
    
    XInclude support in XML configuration files is now disabled by default and 
must
    be enabled with the `log4j2.configurationEnableXInclude` property. The
    constructor reads the property and configures the `DocumentBuilder` 
accordingly,
    dropping the previous always-on behavior and its LOG4J2-1127 "retry without
    XInclude" fallback.
    
    The new property is documented alongside the other configuration-factory
    properties, and the XInclude manual section notes that it is now opt-in.
    
    The `ConfigurationFactoryTest.xinclude` test enables the property, and
    `testGetConfigurationWithMultipleUris` now composes two plain configurations
    instead of incidentally loading an XInclude file.
    
    A new `ConfigurationFactoryUnitTest` loads the same logical configuration as
    XML, JSON, YAML, Java properties, an XInclude-composed file (with the 
property
    enabled) and a cross-format composite, asserting through the `Configuration`
    interface that each yields the same components. It also covers the
    XInclude-disabled fallback to the default configuration.
    
    Assisted-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../core/config/ConfigurationFactoryTest.java      |   4 +-
 .../core/config/ConfigurationFactoryUnitTest.java  | 171 +++++++++++++++++++++
 .../src/test/resources/log4j-test1-basic.xml       |  29 ++++
 .../src/test/resources/log4j-test1-extended.yaml   |  53 +++++++
 .../log4j/core/config/xml/XmlConfiguration.java    |  28 ++--
 src/changelog/.2.x.x/xinclude_opt_in.xml           |  12 ++
 .../partials/manual/configuration-xml-format.adoc  |   8 +
 .../properties-configuration-factory.adoc          |  14 ++
 8 files changed, 299 insertions(+), 20 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..a7232d1f34 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);
@@ -181,7 +183,7 @@ class ConfigurationFactoryTest {
             final URI configLocation1 =
                     getClass().getResource("/log4j-test1.xml").toURI();
             final URI configLocation2 =
-                    getClass().getResource("/log4j-xinclude.xml").toURI();
+                    getClass().getResource("/log4j-test2.xml").toURI();
             final List<URI> configLocations = Arrays.asList(configLocation1, 
configLocation2);
             final Configuration config = factory.getConfiguration(context, 
"test", configLocations);
             assertInstanceOf(CompositeConfiguration.class, config);
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java
new file mode 100644
index 0000000000..b34bf0483a
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.appender.FileAppender;
+import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
+import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.apache.logging.log4j.test.junit.TempLoggingDir;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Verifies that the same logical configuration, expressed as XML, JSON, YAML, 
Java properties, an XInclude-composed
+ * XML file, or a merged configuration produces the same components when 
loaded through {@link ConfigurationFactory}.
+ *
+ * <p>Unlike {@code ConfigurationFactoryTest}, this exercises the {@link 
Configuration} directly.</p>
+ */
+class ConfigurationFactoryUnitTest {
+
+    // The File appender creates the log file at construction time, not at 
startup.
+    // This directory will hold the created files and delete them after the 
test.
+    @TempLoggingDir
+    private static Path loggingDir;
+
+    private static Configuration load(final String resource) throws Exception {
+        final URI uri =
+                ConfigurationFactoryUnitTest.class.getResource("/" + 
resource).toURI();
+        final Configuration configuration =
+                ConfigurationFactory.getInstance().getConfiguration(new 
LoggerContext("test"), resource, uri);
+        configuration.initialize();
+        return configuration;
+    }
+
+    private static Configuration loadComposite(final String... resources) 
throws Exception {
+        final List<URI> uris = new ArrayList<>();
+        for (final String resource : resources) {
+            uris.add(ConfigurationFactoryUnitTest.class
+                    .getResource("/" + resource)
+                    .toURI());
+        }
+        final Configuration configuration =
+                ConfigurationFactory.getInstance().getConfiguration(new 
LoggerContext("test"), "composite", uris);
+        configuration.initialize();
+        return configuration;
+    }
+
+    @ParameterizedTest
+    @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = 
"true")
+    @ValueSource(
+            strings = {
+                "log4j-test1.xml",
+                "log4j-test1.json",
+                "log4j-test1.yaml",
+                "log4j-test1.properties",
+                "log4j-xinclude.xml"
+            })
+    void producesEquivalentComponents(final String resource) throws Exception {
+        final Configuration configuration = load(resource);
+        try {
+            assertAppenders(configuration);
+            assertLoggers(configuration);
+            // Every representation declares a `filename` substitution 
property resolving to a per-format log file.
+            
assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log");
+        } finally {
+            configuration.stop();
+        }
+    }
+
+    /**
+     * Tests behavior with XInclude disabled.
+     *
+     * <p>With XInclude disabled (the default), the {@code xi:include} 
elements are not expanded,
+     * so Log4j ignores them and falls back to the default setup:
+     * an {@code ERROR}-level root logger with a default {@code Console} 
appender.</p>
+     *
+     * <p>The {@code Properties} declared directly in the file are still 
honored.</p>
+     */
+    @Test
+    void xIncludeDisabledIgnoresIncludesAndUsesDefaults() throws Exception {
+        final Configuration configuration = load("log4j-xinclude.xml");
+        try {
+            // None of the included appenders or loggers are present.
+            final Map<String, Appender> appenders = 
configuration.getAppenders();
+            assertThat(appenders).doesNotContainKeys("STDOUT", "File", "List");
+            assertThat(configuration.getLoggers())
+                    .doesNotContainKeys("org.apache.logging.log4j.test1", 
"org.apache.logging.log4j.test2");
+
+            // The default setup is used instead: a single default Console 
appender and an ERROR-level root logger.
+            assertThat(appenders.values()).singleElement().satisfies(appender 
-> {
+                assertThat(appender).isInstanceOf(ConsoleAppender.class);
+                assertThat(appender.getName()).startsWith("DefaultConsole-");
+            });
+            
assertThat(configuration.getRootLogger().getLevel()).isEqualTo(Level.ERROR);
+
+            // The `Properties` declared directly in the file (outside the 
includes) are still resolved.
+            
assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log");
+        } finally {
+            configuration.stop();
+        }
+    }
+
+    /**
+     * The same component graph can be assembled from several sources, 
possibly in different formats, merged into a
+     * {@link CompositeConfiguration}: here an XML base (one appender and the 
root logger) and a YAML remainder.
+     */
+    @Test
+    void mergesMultipleSources() throws Exception {
+        final Configuration configuration = 
loadComposite("log4j-test1-basic.xml", "log4j-test1-extended.yaml");
+        try {
+            
assertThat(configuration).isInstanceOf(CompositeConfiguration.class);
+            assertAppenders(configuration);
+            assertLoggers(configuration);
+            
assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log");
+        } finally {
+            configuration.stop();
+        }
+    }
+
+    private static void assertAppenders(final Configuration configuration) {
+        final Map<String, Appender> appenders = configuration.getAppenders();
+        assertThat(appenders).containsOnlyKeys("STDOUT", "File", "List");
+        
assertThat(appenders.get("STDOUT")).isInstanceOf(ConsoleAppender.class);
+        assertThat(appenders.get("File")).isInstanceOf(FileAppender.class);
+        assertThat(appenders.get("List")).isInstanceOf(ListAppender.class);
+    }
+
+    private static void assertLoggers(final Configuration configuration) {
+        final LoggerConfig test1 = 
configuration.getLoggerConfig("org.apache.logging.log4j.test1");
+        assertThat(test1.getLevel()).isEqualTo(Level.DEBUG);
+        assertThat(test1.isAdditive()).isFalse();
+        
assertThat(test1.getFilter()).isInstanceOf(ThreadContextMapFilter.class);
+        
assertThat(test1.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("STDOUT");
+
+        final LoggerConfig test2 = 
configuration.getLoggerConfig("org.apache.logging.log4j.test2");
+        assertThat(test2.getLevel()).isEqualTo(Level.DEBUG);
+        assertThat(test2.isAdditive()).isFalse();
+        
assertThat(test2.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("File");
+
+        final LoggerConfig root = configuration.getRootLogger();
+        assertThat(root.getLevel()).isEqualTo(Level.ERROR);
+        
assertThat(root.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("STDOUT");
+    }
+}
diff --git a/log4j-core-test/src/test/resources/log4j-test1-basic.xml 
b/log4j-core-test/src/test/resources/log4j-test1-basic.xml
new file mode 100644
index 0000000000..eff1f6b3de
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-test1-basic.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.
+  -->
+<Configuration status="OFF" name="XMLConfigTestBasic">
+  <Appenders>
+    <Console name="STDOUT">
+      <PatternLayout pattern="%m%n"/>
+    </Console>
+  </Appenders>
+  <Loggers>
+    <Root level="error">
+      <AppenderRef ref="STDOUT"/>
+    </Root>
+  </Loggers>
+</Configuration>
diff --git a/log4j-core-test/src/test/resources/log4j-test1-extended.yaml 
b/log4j-core-test/src/test/resources/log4j-test1-extended.yaml
new file mode 100644
index 0000000000..4c23eab343
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-test1-extended.yaml
@@ -0,0 +1,53 @@
+#
+# 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.
+#
+##
+# Remainder of a composite configuration, merged with `log4j-test1-basic.xml` 
(a different format).
+Configuration:
+  status: OFF
+  name: YAMLConfigTestExtended
+  properties:
+    property:
+      name: filename
+      value: ${test:logging.path}/test-composite.log
+  appenders:
+    File:
+      name: File
+      fileName: ${filename}
+      bufferedIO: false
+      PatternLayout:
+        Pattern: "%d %p %C{1.} [%t] %m%n"
+    List:
+      name: List
+      Filters:
+        ThresholdFilter:
+          level: error
+  Loggers:
+    logger:
+      - name: org.apache.logging.log4j.test1
+        level: debug
+        additivity: false
+        ThreadContextMapFilter:
+          KeyValuePair:
+            key: test
+            value: 123
+        AppenderRef:
+          ref: STDOUT
+      - name: org.apache.logging.log4j.test2
+        level: debug
+        additivity: false
+        AppenderRef:
+          ref: File
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 8861171899..e36025f6d7 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
@@ -45,7 +45,7 @@ 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;
@@ -60,6 +60,11 @@ import org.xml.sax.SAXException;
  */
 public class XmlConfiguration extends AbstractConfiguration implements 
Reconfigurable {
 
+    /**
+     * Property that enables XInclude processing in XML configuration files. 
Disabled by default.
+     */
+    private static final String ENABLE_XINCLUDE_PROP = 
"log4j2.configurationEnableXInclude";
+
     private Element rootElement;
     private boolean strict;
     private String schemaResource;
@@ -80,24 +85,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());
diff --git a/src/changelog/.2.x.x/xinclude_opt_in.xml 
b/src/changelog/.2.x.x/xinclude_opt_in.xml
new file mode 100644
index 0000000000..b3ae06d871
--- /dev/null
+++ b/src/changelog/.2.x.x/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">
+    Make XInclude processing in XML configurations opt-in (disabled by 
default): it can be enabled with the `log4j2.configurationEnableXInclude` 
property.
+  </description>
+</entry>
diff --git 
a/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc 
b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc
index 5a19684e08..cd8d423232 100644
--- a/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc
+++ b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc
@@ -54,6 +54,14 @@ This setting also enables "XML strict mode" and allows one 
to specify an element
 XML configuration files can include other files with
 https://www.w3.org/TR/xinclude/[XInclude].
 
+[IMPORTANT]
+====
+Since version `2.27.0`, XInclude processing is **disabled by default**.
+Set the
+xref:manual/systemproperties.adoc#log4j2.configurationEnableXInclude[`log4j2.configurationEnableXInclude`]
+configuration property to `true` to enable it.
+====
+
 NOTE: The list of `XInclude` and `XPath` features supported depends upon your
 
https://docs.oracle.com/javase/{java-target-version}/docs/technotes/guides/xml/jaxp/index.html[JAXP
 implementation].
 
diff --git 
a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
 
b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
index 1841a4ee4d..e9f0f1f7b5 100644
--- 
a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
+++ 
b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
@@ -14,6 +14,20 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 ////
+[id=log4j2.configurationEnableXInclude]
+== `log4j2.configurationEnableXInclude`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_CONFIGURATION_ENABLE_XINCLUDE`
+| Type          | `boolean`
+| Default value | `false`
+|===
+
+If set to `true`, enables https://www.w3.org/TR/xinclude/[XInclude] processing 
in XML configuration files.
+
+See xref:manual/configuration.adoc#xinclude[XInclude] for details.
+
 [id=log4j2.configurationFactory]
 == `log4j2.configurationFactory`
 

Reply via email to