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

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

commit dabbf69250b9ca4d5a56c9ed1b40147e2014e029
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sat Jun 27 18:45:47 2026 +0200

    Resolve XML configuration resources through `ConfigurationSource`
    
    Resolve the resources referenced by an XML configuration through a single
    `ConfigurationSourceResolver`:
    
    * the targets of `xi:include` (as an `EntityResolver`),
    * the validation schema, and
    * the schema's own `xsd:include`/`xsd:import` resources (as an
      `LSResourceResolver`).
    
    They therefore support the Log4j URI conventions (such as the `classpath:`
    scheme) and obey the `log4j2.configurationAllowedProtocols` restrictions. 
The
    schema is parsed with XInclude disabled (a schema is not a configuration), 
and
    a schema violation or a rejected resource fails the configuration with a
    `ConfigurationException`.
    
    Assisted-By: Claude Opus 4.8 <[email protected]>
---
 .../XmlConfigurationConstructHierarchyTest.java    |  34 ++++
 .../config/xml/XmlConfigurationSchemaTest.java     |  80 ++++++++
 .../XmlConfigurationSchemaTest/appenders.xsd       |  31 +++
 .../http-include-config.xml                        |  22 ++
 .../XmlConfigurationSchemaTest/http-include.xsd    |  25 +++
 .../XmlConfigurationSchemaTest/invalid.xml         |  27 +++
 .../resources/XmlConfigurationSchemaTest/main.xsd  |  33 +++
 .../resources/XmlConfigurationSchemaTest/valid.xml |  27 +++
 .../log4j/core/config/xml/XmlConfiguration.java    | 224 ++++++++++++++++++---
 src/changelog/.2.x.x/xinclude_resolver.xml         |  11 +
 .../partials/manual/configuration-xml-format.adoc  |  21 +-
 11 files changed, 499 insertions(+), 36 deletions(-)

diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java
index 73c2019b6c..fdf9fc2139 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationConstructHierarchyTest.java
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.core.config.xml;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
 
@@ -29,13 +30,16 @@ import org.apache.logging.log4j.core.config.Node;
 import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
 import org.apache.logging.log4j.core.config.plugins.util.PluginType;
 import org.apache.logging.log4j.test.ListStatusListener;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
 import org.apache.logging.log4j.test.junit.UsingStatusListener;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.w3c.dom.Element;
 import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
 
 /**
  * Unit tests for {@link XmlConfiguration#constructHierarchy(Node, Element, 
PluginManager, boolean)}.
@@ -304,4 +308,34 @@ class XmlConfigurationConstructHierarchyTest {
             });
         });
     }
+
+    /**
+     * The custom {@code EntityResolver} routes {@code xi:include} through 
{@code ConfigurationSource}, which
+     * understands the {@code classpath:} scheme that the default XInclude 
resolver cannot handle.
+     */
+    @Test
+    void xIncludeResolvesClasspathScheme() throws Exception {
+        givenPlugin("Console");
+        final Element element =
+                
parse(including("classpath:XmlConfigurationConstructHierarchyTest/included.xml",
 null), true);
+
+        final Node appenders = new Node();
+        XmlConfiguration.constructHierarchy(appenders, element, pluginManager, 
false);
+
+        assertThat(appenders.getChildren()).singleElement().satisfies(child -> 
{
+            assertThat(child.getName()).isEqualTo("Console");
+            assertThat(child.getAttributes()).containsEntry("name", "STDOUT");
+        });
+    }
+
+    /**
+     * Checks that {@code xi:include} is subject to {@code ALLOWED_PROTOCOLS} 
restrictions.
+     */
+    @Test
+    @Timeout(5)
+    @SetTestProperty(key = "log4j2.configurationAllowedProtocols", value = 
"file")
+    void xIncludeRespectsAllowedProtocols() {
+        assertThatThrownBy(() -> 
parse(including("http://localhost:12345/evil.xml";, null), true))
+                .isInstanceOf(SAXException.class);
+    }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSchemaTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSchemaTest.java
new file mode 100644
index 0000000000..6ab354c90c
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSchemaTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import java.util.Objects;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.ConfigurationException;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Verifies that, for a {@code strict} XML configuration, the schema and the 
resources it imports are resolved through
+ * {@link ConfigurationSource} (so they honor the Log4j URI conventions and 
the {@code ALLOWED_PROTOCOLS} restrictions),
+ * and that a validation failure breaks the configuration by throwing a {@link 
ConfigurationException}.
+ */
+class XmlConfigurationSchemaTest {
+
+    private static void load(final String name) {
+        final URI uri;
+        try {
+            uri = Objects.requireNonNull(
+                            
XmlConfigurationSchemaTest.class.getResource("/XmlConfigurationSchemaTest/" + 
name))
+                    .toURI();
+        } catch (final Exception e) {
+            throw new AssertionError("Cannot locate test resource " + name, e);
+        }
+        new XmlConfiguration(new LoggerContext("test"), 
ConfigurationSource.fromUri(uri));
+    }
+
+    /**
+     * A conforming configuration validates cleanly. This also proves that the 
schema's {@code xsd:include} is resolved
+     * through the resolver: {@code main.xsd} only compiles if {@code 
appenders.xsd} is found.
+     */
+    @Test
+    void validConfigurationValidates() {
+        assertThatCode(() -> load("valid.xml")).doesNotThrowAnyException();
+    }
+
+    /**
+     * A configuration that violates the schema is rejected. The schema allows 
only a {@code Console} appender, but the
+     * configuration declares a {@code File} appender.
+     */
+    @Test
+    void invalidConfigurationIsRejected() {
+        assertThatThrownBy(() -> 
load("invalid.xml")).isInstanceOf(ConfigurationException.class);
+    }
+
+    /**
+     * Because the schema's {@code xsd:include} resources are resolved through 
{@link ConfigurationSource}, they obey
+     * the {@code ALLOWED_PROTOCOLS} restrictions. With {@code http} excluded, 
an {@code http} include is rejected and
+     * the schema cannot be built. The {@code @Timeout} guards against the 
resolver reaching the network instead of
+     * rejecting the protocol.
+     */
+    @Test
+    @Timeout(5)
+    @SetTestProperty(key = "log4j2.configurationAllowedProtocols", value = 
"file")
+    void schemaIncludeRespectsAllowedProtocols() {
+        assertThatThrownBy(() -> 
load("http-include-config.xml")).isInstanceOf(ConfigurationException.class);
+    }
+}
diff --git 
a/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/appenders.xsd 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/appenders.xsd
new file mode 100644
index 0000000000..6d50587823
--- /dev/null
+++ 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/appenders.xsd
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<!--
+    Included by `main.xsd`; resolving it proves that `xsd:include` resources 
go through the resolver.
+    `Appenders` is restricted to `Console`, so a `File` appender is rejected.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema";>
+  <xs:element name="Console" type="xs:anyType"/>
+  <xs:element name="Appenders">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element ref="Console" maxOccurs="unbounded"/>
+      </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>
diff --git 
a/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/http-include-config.xml
 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/http-include-config.xml
new file mode 100644
index 0000000000..9a41f382c7
--- /dev/null
+++ 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/http-include-config.xml
@@ -0,0 +1,22 @@
+<?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 strict="true" 
schema="XmlConfigurationSchemaTest/http-include.xsd">
+  <Loggers>
+    <Root level="ERROR"/>
+  </Loggers>
+</Configuration>
diff --git 
a/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/http-include.xsd
 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/http-include.xsd
new file mode 100644
index 0000000000..36f4da157d
--- /dev/null
+++ 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/http-include.xsd
@@ -0,0 +1,25 @@
+<?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.
+  -->
+<!--
+    Includes a sub-schema over `http`. With `http` excluded from the allowed 
protocols, the resolver rejects the
+    include instead of fetching it.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema";>
+  <xs:include schemaLocation="http://localhost:12345/evil.xsd"/>
+  <xs:element name="Configuration" type="xs:anyType"/>
+</xs:schema>
diff --git 
a/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/invalid.xml 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/invalid.xml
new file mode 100644
index 0000000000..23e9ba989f
--- /dev/null
+++ b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/invalid.xml
@@ -0,0 +1,27 @@
+<?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 strict="true" schema="XmlConfigurationSchemaTest/main.xsd">
+  <Appenders>
+    <File name="FILE" fileName="target/XmlConfigurationSchemaTest.log"/>
+  </Appenders>
+  <Loggers>
+    <Root level="ERROR">
+      <AppenderRef ref="FILE"/>
+    </Root>
+  </Loggers>
+</Configuration>
diff --git 
a/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/main.xsd 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/main.xsd
new file mode 100644
index 0000000000..3ce2a0328e
--- /dev/null
+++ b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/main.xsd
@@ -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.
+  -->
+<!--
+    A policy schema that allows only a `Console` appender. The `Appenders` 
element is defined in the included
+    `appenders.xsd`, so the schema only compiles if the `xsd:include` is 
resolved through the resolver.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema";>
+  <xs:include schemaLocation="appenders.xsd"/>
+  <xs:element name="Configuration">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element ref="Appenders"/>
+        <xs:element name="Loggers" type="xs:anyType"/>
+      </xs:sequence>
+      <xs:anyAttribute processContents="skip"/>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>
diff --git 
a/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/valid.xml 
b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/valid.xml
new file mode 100644
index 0000000000..266cf1f5d9
--- /dev/null
+++ b/log4j-core-test/src/test/resources/XmlConfigurationSchemaTest/valid.xml
@@ -0,0 +1,27 @@
+<?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 strict="true" schema="XmlConfigurationSchemaTest/main.xsd">
+  <Appenders>
+    <Console name="STDOUT"/>
+  </Appenders>
+  <Loggers>
+    <Root level="ERROR">
+      <AppenderRef ref="STDOUT"/>
+    </Root>
+  </Loggers>
+</Configuration>
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 56a8fabef8..da6085e630 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
@@ -19,6 +19,9 @@ package org.apache.logging.log4j.core.config.xml;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.Reader;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
@@ -27,13 +30,14 @@ import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.stream.StreamSource;
+import javax.xml.transform.dom.DOMSource;
 import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
 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;
@@ -43,7 +47,7 @@ import 
org.apache.logging.log4j.core.config.status.StatusConfiguration;
 import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings;
 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.NetUtils;
 import org.apache.logging.log4j.core.util.Patterns;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.w3c.dom.Attr;
@@ -52,6 +56,10 @@ import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.NodeList;
 import org.w3c.dom.Text;
+import org.w3c.dom.ls.LSException;
+import org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.EntityResolver;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
@@ -86,8 +94,7 @@ public class XmlConfiguration extends AbstractConfiguration 
implements Reconfigu
             final InputSource source = new InputSource(new 
ByteArrayInputStream(buffer));
             source.setSystemId(configSource.getLocation());
             final boolean xIncludeAware = 
PropertiesUtil.getProperties().getBooleanProperty(ENABLE_XINCLUDE_PROP);
-            final DocumentBuilder documentBuilder = 
newDocumentBuilder(xIncludeAware);
-            final Document document = documentBuilder.parse(source);
+            final Document document = 
newDocumentBuilder(xIncludeAware).parse(source);
             rootElement = document.getDocumentElement();
             final Map<String, String> attrs = processAttributes(rootNode, 
rootElement);
             final StatusConfiguration statusConfig = new 
StatusConfiguration().withStatus(getDefaultStatus());
@@ -119,35 +126,11 @@ public class XmlConfiguration extends 
AbstractConfiguration implements Reconfigu
             }
             initializeWatchers(this, configSource, monitorIntervalSeconds);
             statusConfig.initialize();
-        } catch (final SAXException | IOException | 
ParserConfigurationException e) {
-            LOGGER.error("Error parsing " + configSource.getLocation(), e);
-        }
-        if (strict && schemaResource != null && buffer != null) {
-            try (final InputStream is =
-                    Loader.getResourceAsStream(schemaResource, 
XmlConfiguration.class.getClassLoader())) {
-                if (is != null) {
-                    final javax.xml.transform.Source src = new 
StreamSource(is, schemaResource);
-                    final SchemaFactory factory = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
-                    Schema schema = null;
-                    try {
-                        schema = factory.newSchema(src);
-                    } catch (final SAXException ex) {
-                        LOGGER.error("Error parsing Log4j schema", ex);
-                    }
-                    if (schema != null) {
-                        final Validator validator = schema.newValidator();
-                        try {
-                            validator.validate(new StreamSource(new 
ByteArrayInputStream(buffer)));
-                        } catch (final IOException ioe) {
-                            LOGGER.error("Error reading configuration for 
validation", ioe);
-                        } catch (final SAXException ex) {
-                            LOGGER.error("Error validating configuration", ex);
-                        }
-                    }
-                }
-            } catch (final Exception ex) {
-                LOGGER.error("Unable to access schema {}", 
this.schemaResource, ex);
+            if (strict && schemaResource != null) {
+                validateDocument(document, "classpath:" + schemaResource);
             }
+        } catch (final SAXException | LSException | 
ParserConfigurationException | IOException e) {
+            LOGGER.error("Error parsing " + configSource.getLocation(), e);
         }
 
         if (getName() == null) {
@@ -171,7 +154,13 @@ public class XmlConfiguration extends 
AbstractConfiguration implements Reconfigu
         if (xIncludeAware) {
             factory.setXIncludeAware(true);
         }
-        return factory.newDocumentBuilder();
+        final DocumentBuilder builder = factory.newDocumentBuilder();
+        if (xIncludeAware) {
+            // Resolve the resources referenced by `xi:include` through 
`ConfigurationSource`, so they honor the Log4j
+            // URI conventions (e.g. the `classpath:` scheme) and the 
`ALLOWED_PROTOCOLS` restrictions.
+            builder.setEntityResolver(ConfigurationSourceResolver.INSTANCE);
+        }
+        return builder;
     }
 
     private static void disableDtdProcessing(final DocumentBuilderFactory 
factory) {
@@ -195,6 +184,33 @@ public class XmlConfiguration extends 
AbstractConfiguration implements Reconfigu
         }
     }
 
+    private static void validateDocument(final Document document, final String 
schemaLocation)
+            throws ConfigurationException {
+        try {
+            // Resolve the schema, and the resources it imports, through the 
same resolver as the configuration file.
+            final ConfigurationSource schemaSource = 
ConfigurationSource.fromUri(NetUtils.toURI(schemaLocation));
+            if (schemaSource != null) {
+                // Parse the schema with XInclude disabled:
+                // a schema has its own modularity features 
(`xsd:include`/`xsd:import`).
+                final Document schemaDocument =
+                        
newDocumentBuilder(false).parse(ConfigurationSourceResolver.toInputSource(schemaSource));
+                final SchemaFactory factory = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+                
factory.setResourceResolver(ConfigurationSourceResolver.INSTANCE);
+                // The system id is the base URI against which the schema's 
`xsd:include`/`xsd:import` resources
+                // are resolved by the resource resolver above.
+                final Schema schema = factory.newSchema(new 
DOMSource(schemaDocument, schemaSource.getLocation()));
+                final Validator validator = schema.newValidator();
+                
validator.setResourceResolver(ConfigurationSourceResolver.INSTANCE);
+                validator.validate(new DOMSource(document));
+            } else {
+                throw new ConfigurationException("Failed to load XML schema 
from " + schemaLocation);
+            }
+        } catch (LSException | SAXException | ParserConfigurationException | 
IOException e) {
+            throw new ConfigurationException(
+                    "Error validating " + document.getBaseURI() + " using 
schema " + schemaLocation, e);
+        }
+    }
+
     @Override
     public void setup() {
         if (rootElement == null) {
@@ -299,4 +315,146 @@ public class XmlConfiguration extends 
AbstractConfiguration implements Reconfigu
         return getClass().getSimpleName() + "[location=" + 
getConfigurationSource() + ", lastModified="
                 + 
Instant.ofEpochMilli(getConfigurationSource().getLastModified()) + "]";
     }
+
+    /**
+     * Resolves the resources referenced by an XML configuration through 
{@link ConfigurationSource}, the same way the
+     * configuration file itself is resolved: the targets of {@code 
xi:include} (as an {@link EntityResolver}) and the
+     * resources imported by an XML Schema (as an {@link LSResourceResolver}).
+     *
+     * <p>This adds support for the Log4j URI conventions (such as the {@code 
classpath:} scheme) and subjects every
+     * referenced resource to the {@code ALLOWED_PROTOCOLS} restrictions.</p>
+     */
+    private static final class ConfigurationSourceResolver implements 
EntityResolver, LSResourceResolver {
+
+        private static final ConfigurationSourceResolver INSTANCE = new 
ConfigurationSourceResolver();
+
+        /**
+         * Resolves an external entity, used while expanding {@code 
xi:include} elements.
+         */
+        @Override
+        public InputSource resolveEntity(final String publicId, final String 
systemId) throws SAXException {
+            final InputSource inputSource = 
toInputSource(toConfigurationSource(systemId, null));
+            inputSource.setPublicId(publicId);
+            return inputSource;
+        }
+
+        /**
+         * Resolves a resource imported by an XML Schema ({@code 
xsd:import}/{@code xsd:include}).
+         *
+         * <p>Throws an {@link LSException} when the resource cannot be 
resolved, instead of returning {@code null}: a
+         * {@code null} return would let the parser fall back to its own URL 
resolution, bypassing the
+         * {@code ALLOWED_PROTOCOLS} restrictions.</p>
+         */
+        @Override
+        public LSInput resolveResource(
+                final String type,
+                final String namespaceURI,
+                final String publicId,
+                final String systemId,
+                final String baseURI) {
+            try {
+                return new 
ConfigurationSourceLSInput(toConfigurationSource(systemId, baseURI));
+            } catch (final SAXException e) {
+                throw new LSException(LSException.PARSE_ERR, e.getMessage());
+            }
+        }
+
+        private static ConfigurationSource toConfigurationSource(final String 
systemId, final String baseURI)
+                throws SAXException {
+            if (systemId == null) {
+                throw new SAXException("System id missing.");
+            }
+            try {
+                final URI uri = baseURI != null ? new 
URI(baseURI).resolve(systemId) : new URI(systemId);
+                final ConfigurationSource configurationSource = 
ConfigurationSource.fromUri(uri);
+                if (configurationSource == null) {
+                    throw new SAXException("Unable to resolve system id " + 
systemId);
+                }
+                return configurationSource;
+            } catch (final URISyntaxException e) {
+                throw new SAXException("System id is not a valid URI: " + 
systemId, e);
+            }
+        }
+
+        static InputSource toInputSource(final ConfigurationSource 
configurationSource) {
+            final InputSource inputSource = new 
InputSource(configurationSource.getInputStream());
+            inputSource.setSystemId(configurationSource.getLocation());
+            return inputSource;
+        }
+    }
+
+    /**
+     * Minimal {@link LSInput} backed by a {@link ConfigurationSource}.
+     */
+    private static final class ConfigurationSourceLSInput implements LSInput {
+        private final ConfigurationSource configurationSource;
+
+        ConfigurationSourceLSInput(final ConfigurationSource 
configurationSource) {
+            this.configurationSource = configurationSource;
+        }
+
+        @Override
+        public InputStream getByteStream() {
+            return configurationSource.getInputStream();
+        }
+
+        @Override
+        public String getSystemId() {
+            return configurationSource.getLocation();
+        }
+
+        @Override
+        public String getPublicId() {
+            return null;
+        }
+
+        @Override
+        public String getBaseURI() {
+            return null;
+        }
+
+        @Override
+        public Reader getCharacterStream() {
+            return null;
+        }
+
+        @Override
+        public String getStringData() {
+            return null;
+        }
+
+        @Override
+        public String getEncoding() {
+            return null;
+        }
+
+        @Override
+        public boolean getCertifiedText() {
+            return false;
+        }
+
+        @Override
+        public void setByteStream(final InputStream byteStream) {}
+
+        @Override
+        public void setCharacterStream(final Reader characterStream) {}
+
+        @Override
+        public void setStringData(final String stringData) {}
+
+        @Override
+        public void setSystemId(final String systemId) {}
+
+        @Override
+        public void setPublicId(final String publicId) {}
+
+        @Override
+        public void setBaseURI(final String baseURI) {}
+
+        @Override
+        public void setEncoding(final String encoding) {}
+
+        @Override
+        public void setCertifiedText(final boolean certifiedText) {}
+    }
 }
diff --git a/src/changelog/.2.x.x/xinclude_resolver.xml 
b/src/changelog/.2.x.x/xinclude_resolver.xml
new file mode 100644
index 0000000000..7f1ef6db54
--- /dev/null
+++ b/src/changelog/.2.x.x/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">
+    Resolve the resources referenced by an XML configuration through 
`ConfigurationSource`.
+  </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 cd8d423232..b2850f5d86 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
@@ -34,6 +34,11 @@ The XML format supports the following additional attributes 
on the `Configuratio
 
 Specifies the path to a classpath resource containing an XML schema.
 
+The schema's own `xsd:include`/`xsd:import` resources are resolved through the 
same mechanism as the configuration
+file itself, so they support the Log4j URI conventions, such as the 
`classpath:` scheme, and are subject to the
+xref:manual/systemproperties.adoc#log4j2.configurationAllowedProtocols[`log4j2.configurationAllowedProtocols`]
+restrictions.
+
 [id=configuration-attribute-strict]
 === `strict`
 
@@ -43,8 +48,11 @@ Specifies the path to a classpath resource containing an XML 
schema.
 | Default value | `false`
 |===
 
-If set to `true,` all configuration files will be checked against the XML 
schema provided by the
-<<configuration-attribute-schema>>.
+If set to `true`, all configuration files are checked against the XML schema 
provided by the
+<<configuration-attribute-schema>>, and a configuration that does not conform 
to it is rejected.
+
+NOTE: Schema validation is a configuration-authoring aid, not a security 
control.
+The configuration file is trusted, so validation offers no protection against 
a configuration that an attacker can already modify.
 
 This setting also enables "XML strict mode" and allows one to specify an 
element's **plugin type** through a `type` attribute instead of the tag name.
 
@@ -56,10 +64,17 @@ https://www.w3.org/TR/xinclude/[XInclude].
 
 [IMPORTANT]
 ====
-Since version `2.27.0`, XInclude processing is **disabled by default**.
+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.
+* The `href` of an `xi:include` is resolved through the same mechanism as the 
configuration file itself.
+It therefore supports the Log4j URI conventions, such as the `classpath:` 
scheme,
+and is subject to the
+xref:manual/systemproperties.adoc#log4j2.configurationAllowedProtocols[`log4j2.configurationAllowedProtocols`]
+restrictions.
 ====
 
 NOTE: The list of `XInclude` and `XPath` features supported depends upon your


Reply via email to