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

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 5bfa162bd5 Cleanup `XMLInputFactory` usages. Set `ACCESS_EXTERNAL_DTD` 
property to a value that exclude the "file" protocol.
5bfa162bd5 is described below

commit 5bfa162bd56edb7d41d56a0c926592964d31d83b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Dec 13 02:24:59 2025 +0100

    Cleanup `XMLInputFactory` usages.
    Set `ACCESS_EXTERNAL_DTD` property to a value that exclude the "file" 
protocol.
---
 .../org.apache.sis.metadata/main/module-info.java  |   1 +
 .../main/org/apache/sis/xml/PooledMarshaller.java  |   1 +
 .../org/apache/sis/xml/PooledUnmarshaller.java     |   1 +
 .../xml/internal/shared/ExternalLinkHandler.java   |   4 +-
 .../xml/{ => internal/shared}/InputFactory.java    |  27 +++-
 .../xml/{ => internal/shared}/OutputFactory.java   |  10 +-
 .../test/org/apache/sis/xml/LegacyCodesTest.java   |   1 +
 .../org/apache/sis/xml/MarshallerPoolTest.java     |   1 +
 .../test/org/apache/sis/xml/NamespacesTest.java    |   1 +
 .../test/org/apache/sis/xml/NilReasonTest.java     |   3 +-
 .../test/org/apache/sis/xml/TransformerTest.java   |   1 +
 .../apache/sis/xml/TransformingNamespacesTest.java |   1 +
 .../org/apache/sis/xml/ValueConverterTest.java     |   1 +
 .../test/org/apache/sis/xml/XLinkTest.java         |   3 +-
 .../test/org/apache/sis/xml/XPointerTest.java      |   1 +
 .../sis/xml/internal/shared/InputFactoryTest.java  | 160 +++++++++++++++++++++
 .../sis/storage/geotiff/reader/XMLMetadata.java    |   7 +-
 .../org/apache/sis/storage/gpx/WritableStore.java  |   2 +-
 .../apache/sis/storage/xml/stream/InputType.java   |   6 +-
 .../apache/sis/storage/xml/stream/OutputType.java  |   6 +-
 .../sis/storage/xml/stream/RewriteOnUpdate.java    |   2 +-
 .../sis/storage/xml/stream/StaxDataStore.java      |  74 +++++-----
 .../sis/storage/xml/stream/StaxStreamWriter.java   |   4 +-
 .../org/apache/sis/storage/base/URIDataStore.java  |   5 +-
 24 files changed, 260 insertions(+), 63 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/module-info.java 
b/endorsed/src/org.apache.sis.metadata/main/module-info.java
index e76f65657f..31f8ae9eb3 100644
--- a/endorsed/src/org.apache.sis.metadata/main/module-info.java
+++ b/endorsed/src/org.apache.sis.metadata/main/module-info.java
@@ -173,6 +173,7 @@ module org.apache.sis.metadata {
             org.apache.sis.referencing,
             org.apache.sis.storage,
             org.apache.sis.storage.xml,
+            org.apache.sis.storage.geotiff,
             org.apache.sis.gui;                 // In the "optional" 
sub-project.
 
     /*
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
index b9a5cce3bf..eb619cfc6c 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
@@ -38,6 +38,7 @@ import org.xml.sax.ContentHandler;
 import org.w3c.dom.Node;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.bind.UseLegacyMetadata;
+import org.apache.sis.xml.internal.shared.OutputFactory;
 import org.apache.sis.xml.internal.shared.ExternalLinkHandler;
 
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
index 00eead6dd6..84e86f0793 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
@@ -44,6 +44,7 @@ import jakarta.xml.bind.attachment.AttachmentUnmarshaller;
 import org.w3c.dom.Node;
 import org.xml.sax.InputSource;
 import org.apache.sis.xml.bind.Context;
+import org.apache.sis.xml.internal.shared.InputFactory;
 import org.apache.sis.xml.internal.shared.ExternalLinkHandler;
 import org.apache.sis.util.resources.Errors;
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
index 068f6a6b27..5aa850b588 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
@@ -283,7 +283,7 @@ public class ExternalLinkHandler {
         if (!(property instanceof XMLResolver)) {
             return new ExternalLinkHandler(base);
         }
-        final XMLResolver resolver = (XMLResolver) property;
+        final var resolver = (XMLResolver) property;
         return new ExternalLinkHandler(base) {
             @Override public Source openReader(final URI path) throws 
Exception {
                 /*
@@ -311,6 +311,8 @@ public class ExternalLinkHandler {
 
     /**
      * Returns a string representation of this link handler for debugging 
purposes.
+     *
+     * @return a string representation for debugging purposes.
      */
     @Override
     public String toString() {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/InputFactory.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/InputFactory.java
similarity index 81%
rename from 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/InputFactory.java
rename to 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/InputFactory.java
index 6e193fcf80..1fd6bd6437 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/InputFactory.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/InputFactory.java
@@ -14,10 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.xml;
+package org.apache.sis.xml.internal.shared;
 
 import java.io.Reader;
 import java.io.InputStream;
+import javax.xml.XMLConstants;
 import javax.xml.stream.XMLEventReader;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamReader;
@@ -35,17 +36,31 @@ import org.xml.sax.InputSource;
  * This convenience is provided in a separated class in order to allow the JVM 
to instantiate the factory
  * only when first needed, when initializing this class.
  *
+ * <h2>Security</h2>
+ * Unless the user has configured the {@code javax.xml.accessExternalDTD} 
property to something else
+ * than {@code "all"}, this class disallows external DTDs referenced by the 
{@code "file"} protocols.
+ * Allowed protocols are <abbr>HTTP</abbr> and <abbr>HTTPS</abbr> (list may be 
expanded if needed).
+ *
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see <a href="https://openjdk.org/jeps/185";>JEP 185: Restrict Fetching of 
External XML Resources</a>
  */
-final class InputFactory {
+public final class InputFactory {
     /**
      * The SIS-wide factory. This factory can be specified by the user, for 
example using the
-     * {@code javax.xml.stream.XMLInputFactory} system property.
+     * {@code javax.xml.stream.XMLInputFactory} system property or with {@code 
META-INF/services}.
      *
-     * <div class="note"><b>Note:</b>
-     * {@code XMLInputFactory} has an {@code newDefaultFactory()} method which 
bypass user settings.</div>
+     * @see org.apache.sis.storage.xml.stream.StaxDataStore#inputFactory()
      */
     private static final XMLInputFactory FACTORY = 
XMLInputFactory.newInstance();
+    static {
+        if 
(FACTORY.isPropertySupported(XMLConstants.FEATURE_SECURE_PROCESSING)) {
+            FACTORY.setProperty(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
+        }
+        if 
("all".equals(FACTORY.getProperty(XMLConstants.ACCESS_EXTERNAL_DTD))) {
+            FACTORY.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, 
"http,https");
+        }
+    }
 
     /**
      * Do not allow instantiation of this class.
@@ -68,7 +83,7 @@ final class InputFactory {
      * @throws XMLStreamException if the reader cannot be created.
      */
     public static XMLEventReader createXMLEventReader(final InputStream in) 
throws XMLStreamException {
-        return FACTORY.createXMLEventReader(in);
+        return FACTORY.createXMLEventReader(in, "UTF-8");
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/OutputFactory.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/OutputFactory.java
similarity index 95%
rename from 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/OutputFactory.java
rename to 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/OutputFactory.java
index 9818c0dad3..6a9e53f025 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/OutputFactory.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/OutputFactory.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.xml;
+package org.apache.sis.xml.internal.shared;
 
 import java.io.OutputStream;
 import java.io.Writer;
@@ -28,7 +28,6 @@ import javax.xml.transform.sax.SAXResult;
 import javax.xml.transform.stax.StAXResult;
 import org.w3c.dom.Node;
 import org.xml.sax.ContentHandler;
-import org.apache.sis.xml.internal.shared.StreamWriterDelegate;
 
 
 /**
@@ -38,13 +37,10 @@ import 
org.apache.sis.xml.internal.shared.StreamWriterDelegate;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class OutputFactory {
+public final class OutputFactory {
     /**
      * The SIS-wide factory. This factory can be specified by the user, for 
example using the
-     * {@code javax.xml.stream.XMLOutputFactory} system property.
-     *
-     * <div class="note"><b>Note:</b>
-     * {@code XMLOutputFactory} has an {@code newDefaultFactory()} method 
which bypass user settings.</div>
+     * {@code javax.xml.stream.XMLOutputFactory} system property or with 
{@code META-INF/services}.
      */
     private static final XMLOutputFactory FACTORY = 
XMLOutputFactory.newInstance();
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/LegacyCodesTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/LegacyCodesTest.java
index 7a8f400963..28254b2936 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/LegacyCodesTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/LegacyCodesTest.java
@@ -27,6 +27,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class LegacyCodesTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/MarshallerPoolTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/MarshallerPoolTest.java
index 8200948563..e32ec53158 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/MarshallerPoolTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/MarshallerPoolTest.java
@@ -31,6 +31,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class MarshallerPoolTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NamespacesTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NamespacesTest.java
index 8adb8f9cd0..37eef8de53 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NamespacesTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NamespacesTest.java
@@ -29,6 +29,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class NamespacesTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
index 6ad32a7d81..364c251248 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
@@ -34,10 +34,11 @@ import org.opengis.metadata.citation.Responsibility;
 
 
 /**
- * Tests the {@link NilReason}.
+ * Tests {@link NilReason}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class NilReasonTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformerTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformerTest.java
index 5bd3ad013c..6a07570429 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformerTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformerTest.java
@@ -32,6 +32,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class TransformerTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformingNamespacesTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformingNamespacesTest.java
index b3ef68acd9..7aa72a61b5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformingNamespacesTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/TransformingNamespacesTest.java
@@ -36,6 +36,7 @@ import static org.apache.sis.test.Assertions.assertSetEquals;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class TransformingNamespacesTest extends TestCase implements 
NamespaceContext {
     /**
      * All prefixes declared in this test.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ValueConverterTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ValueConverterTest.java
index 700ccca3a4..a02f6de7eb 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ValueConverterTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ValueConverterTest.java
@@ -33,6 +33,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ValueConverterTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XLinkTest.java 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XLinkTest.java
index c118b1dbc6..67703036bd 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XLinkTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XLinkTest.java
@@ -28,10 +28,11 @@ import static 
org.apache.sis.test.Assertions.assertMessageContains;
 
 
 /**
- * Tests the {@link XLink}.
+ * Tests {@link XLink}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class XLinkTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XPointerTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XPointerTest.java
index 74ed67096e..f0e35a627d 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XPointerTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/XPointerTest.java
@@ -29,6 +29,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class XPointerTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/internal/shared/InputFactoryTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/internal/shared/InputFactoryTest.java
new file mode 100644
index 0000000000..82ef0427e3
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/internal/shared/InputFactoryTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.sis.xml.internal.shared;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import javax.xml.XMLConstants;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
+import static org.apache.sis.test.Assertions.assertMessageContains;
+import org.apache.sis.test.TestCase;
+
+
+/**
+ * Tests related to {@link InputFactory}.
+ *
+ * <h2>Security</h2>
+ * This class checks the fix for the "XML External Entity Prevention" 
vulnerability.
+ * Users of Apache <abbr>SIS</abbr> versions without this fix can avoid the 
vulnerability
+ * by setting the {@code javax.xml.accessExternalDTD} property to a value such 
as {@code ""}
+ * (for blocking all accesses) or {@code "https"} (for allowing 
<abbr>HTTPS</abbr> accesses)
+ * at <abbr>JVM</abbr> launch time.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see <a href="https://openjdk.org/jeps/185";>JEP 185: Restrict Fetching of 
External XML Resources</a>
+ * @see <a 
href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html";>XML
 External Entity Prevention</a>
+ */
+@SuppressWarnings("exports")
+public final class InputFactoryTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public InputFactoryTest() {
+    }
+
+    /**
+     * Verifies the capability to access an external <abbr>XML</abbr> entity.
+     * This is used as a reference for testing the effect of different options
+     * in other test cases.
+     *
+     * @throws IOException if an error occurred while writing the test file.
+     * @throws XMLStreamException if an error occurred while parsing the 
<abbr>XML</abbr>.
+     */
+    @Test
+    public void verifyExternalEntityAccess() throws IOException, 
XMLStreamException {
+        final XMLInputFactory factory = XMLInputFactory.newInstance();
+        
assumeTrue("all".equals(factory.getProperty(XMLConstants.ACCESS_EXTERNAL_DTD)));
+        
assumeTrue(Boolean.TRUE.equals(factory.getProperty(XMLInputFactory.SUPPORT_DTD)));
+        
assumeTrue(Boolean.TRUE.equals(factory.getProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES)));
+        readExternalEntity(factory, false);
+    }
+
+    /**
+     * Verifies that the {@code IS_SUPPORTING_EXTERNAL_ENTITIES} property 
blocks the access to the local file.
+     * Instead, the value is {@code null}.
+     *
+     * @throws IOException if an error occurred while writing the test file.
+     * @throws XMLStreamException if an error occurred while parsing the 
<abbr>XML</abbr>.
+     */
+    @Test
+    public void testDisableExternalEntities() throws IOException, 
XMLStreamException {
+        final XMLInputFactory factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
Boolean.FALSE);
+        readExternalEntity(factory, true);
+    }
+
+    /**
+     * Verifies that the {@code SUPPORT_DTD} property blocks the access to the 
local file.
+     * Instead, an {@link XMLStreamException} is thrown.
+     *
+     * @throws IOException if an error occurred while writing the test file.
+     */
+    @Test
+    public void testDisableDTD() throws IOException {
+        final XMLInputFactory factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+        var ex = assertThrows(XMLStreamException.class, () -> 
readExternalEntity(factory, false));
+        assertMessageContains(ex, "xxe");
+    }
+
+    /**
+     * Verifies that the {@code SUPPORT_DTD} property blocks the access to the 
local file.
+     * Instead, an {@link XMLStreamException} is thrown.
+     *
+     * @throws IOException if an error occurred while writing the test file.
+     */
+    @Test
+    public void testDisableAccessExternalDTD() throws IOException {
+        final XMLInputFactory factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "http,https");
+        var ex = assertThrows(XMLStreamException.class, () -> 
readExternalEntity(factory, false));
+        assertMessageContains(ex, "sis-");          // Prefix of the temporary 
file.
+    }
+
+    /**
+     * Tries to get the content of a temporary file on the local file by 
parsing a <abbr>XML</abbr> document.
+     *
+     * @param  factory  the factory to use for attempting an attack.
+     * @param  blocked  whether we expect the access to be blocked.
+     * @throws IOException if an error occurred while writing the test file.
+     * @throws XMLStreamException if an error occurred while parsing the 
<abbr>XML</abbr>.
+     */
+    private void readExternalEntity(final XMLInputFactory factory, final 
boolean blocked) throws IOException, XMLStreamException {
+        String content = null;
+        final String text = "Some text which should be inaccessible.";
+        final Path path = Files.createTempFile("sis-", ".tmp");
+        try {
+            Files.writeString(path, text);
+            final String xml = String.format(
+                    "<!DOCTYPE foo [%n" +
+                    "   <!ELEMENT foo ANY >%n" +
+                    "   <!ENTITY xxe SYSTEM \"%s\" >%n" +
+                    "]>%n" +
+                    "<foo>&xxe;</foo>%n", path.toUri());
+
+            final XMLStreamReader reader = factory.createXMLStreamReader(new 
StringReader(xml));
+            while (reader.hasNext()) {
+                int event = reader.next();
+                if (event == XMLStreamReader.CHARACTERS) {
+                    String found = reader.getText();
+                    if (found != null && !(found = found.trim()).isEmpty()) {
+                        assertNull(content);    // We expect only one 
occurrence.
+                        content = found;
+                    }
+                }
+            }
+            reader.close();
+        } finally {
+            Files.delete(path);
+        }
+        if (blocked) {
+            assertNull(content);
+        } else {
+            assertEquals(text, content);
+        }
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
index d4c8376956..8abcbb0e7b 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
@@ -30,7 +30,6 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import javax.xml.stream.XMLEventReader;
-import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.events.XMLEvent;
 import javax.xml.stream.events.Attribute;
@@ -49,6 +48,7 @@ import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.xml.XML;
+import org.apache.sis.xml.internal.shared.InputFactory;
 
 
 /**
@@ -210,11 +210,10 @@ public final class XMLMetadata implements Filter {
      * Returns a reader for the XML document, or {@code null} if the document 
could not be read.
      */
     private XMLEventReader toXML() throws XMLStreamException {
-        final XMLInputFactory factory = XMLInputFactory.newFactory();
         if (bytes != null) {
-            return factory.createXMLEventReader(new 
ByteArrayInputStream(bytes), "UTF-8");
+            return InputFactory.createXMLEventReader(new 
ByteArrayInputStream(bytes));
         } else if (string != null) {
-            return factory.createXMLEventReader(new StringReader(string));
+            return InputFactory.createXMLEventReader(new StringReader(string));
         } else {
             return null;
         }
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java
index 71a4317fef..8b182d90be 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/WritableStore.java
@@ -156,7 +156,7 @@ public final class WritableStore extends Store implements 
WritableFeatureSet {
             /*
              * Get the writer if no read or other write operation is in 
progress, then write the data.
              */
-            try (Writer writer = new Writer(this, 
org.apache.sis.storage.gpx.Metadata.castOrCopy(metadata, locale), null)) {
+            try (Writer writer = new Writer(this, 
org.apache.sis.storage.gpx.Metadata.castOrCopy(metadata, dataLocale), null)) {
                 writer.writeStartDocument();
                 if (features != null) {
                     features.forEachOrdered(writer);
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/InputType.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/InputType.java
index 82acfc8ddc..0f7746eb14 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/InputType.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/InputType.java
@@ -18,6 +18,7 @@ package org.apache.sis.storage.xml.stream;
 
 import java.io.Reader;
 import java.io.InputStream;
+import java.nio.charset.Charset;
 import javax.xml.stream.XMLEventReader;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamReader;
@@ -60,8 +61,9 @@ enum InputType {
     STREAM(InputStream.class) {
         @Override XMLStreamReader create(StaxDataStore ds, Object s) throws 
XMLStreamException {
             final XMLInputFactory f = ds.inputFactory();
-            return (ds.encoding != null) ? 
f.createXMLStreamReader((InputStream) s, ds.encoding.name())
-                                         : 
f.createXMLStreamReader((InputStream) s);
+            final Charset encoding = ds.getEncoding();
+            return (encoding != null) ? f.createXMLStreamReader((InputStream) 
s, encoding.name())
+                                      : f.createXMLStreamReader((InputStream) 
s);
         }
     },
 
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/OutputType.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/OutputType.java
index 33247753b0..511027cee3 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/OutputType.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/OutputType.java
@@ -23,6 +23,7 @@ import java.io.OutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
+import java.nio.charset.Charset;
 import javax.xml.stream.XMLEventWriter;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamWriter;
@@ -61,8 +62,9 @@ enum OutputType {
     STREAM(OutputStream.class, InputType.STREAM) {
         @Override XMLStreamWriter create(StaxDataStore ds, Object s) throws 
XMLStreamException {
             final XMLOutputFactory f = ds.outputFactory();
-            return (ds.encoding != null) ? 
f.createXMLStreamWriter((OutputStream) s, ds.encoding.name())
-                                         : 
f.createXMLStreamWriter((OutputStream) s);
+            final Charset encoding = ds.getEncoding();
+            return (encoding != null) ? f.createXMLStreamWriter((OutputStream) 
s, encoding.name())
+                                      : f.createXMLStreamWriter((OutputStream) 
s);
         }
         @Override Closeable snapshot(final Object s) {
             if (s instanceof ByteArrayOutputStream) {
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/RewriteOnUpdate.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/RewriteOnUpdate.java
index 1a1839bb4e..7885bbaa6c 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/RewriteOnUpdate.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/RewriteOnUpdate.java
@@ -98,7 +98,7 @@ public abstract class RewriteOnUpdate implements 
AutoCloseable {
      * @return the data locale, or {@code null}.
      */
     protected final Locale getLocale() {
-        return (source instanceof StaxDataStore) ? ((StaxDataStore) 
source).locale : null;
+        return (source instanceof StaxDataStore) ? ((StaxDataStore) 
source).getDataLocale() : null;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
index 458f23c2aa..2c72f7f75a 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
@@ -20,7 +20,6 @@ import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.logging.Filter;
-import java.time.ZoneId;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
@@ -28,6 +27,7 @@ import java.io.OutputStream;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.nio.charset.Charset;
+import javax.xml.XMLConstants;
 import javax.xml.stream.Location;
 import javax.xml.stream.XMLReporter;
 import javax.xml.stream.XMLInputFactory;
@@ -63,28 +63,6 @@ import org.apache.sis.io.wkt.WKTFormat;
  * @author  Martin Desruisseaux (Geomatys)
  */
 public abstract class StaxDataStore extends URIDataStore {
-    /**
-     * The locale to use for locale-sensitive data (<strong>not</strong> for 
logging or warning messages),
-     * or {@code null} if unspecified.
-     *
-     * @see OptionKey#LOCALE
-     */
-    protected final Locale locale;
-
-    /**
-     * The timezone to use when parsing or formatting dates and times without 
explicit timezone,
-     * or {@code null} if unspecified.
-     *
-     * @see OptionKey#TIMEZONE
-     */
-    protected final ZoneId timezone;
-
-    /**
-     * The character encoding of the file content, or {@code null} if 
unspecified.
-     * This is often (but not always) ignored at reading time, but taken in 
account at writing time.
-     */
-    protected final Charset encoding;
-
     /**
      * Configuration information for JAXB (un)marshaller (actually the SIS 
wrappers) or for the StAX factories.
      * This object is a read-only map which may contain the following entries:
@@ -201,9 +179,6 @@ public abstract class StaxDataStore extends URIDataStore {
         super(provider, connector);
         final Integer indent;
         storage         = connector.getStorage();
-        locale          = connector.getOption(OptionKey.LOCALE);
-        timezone        = connector.getOption(OptionKey.TIMEZONE);
-        encoding        = connector.getOption(OptionKey.ENCODING);
         indent          = connector.getOption(OptionKey.INDENTATION);
         indentation     = (indent == null) ? Constants.DEFAULT_INDENTATION
                                            : (byte) 
Math.max(WKTFormat.SINGLE_LINE, Math.min(120, indent));
@@ -298,7 +273,7 @@ public abstract class StaxDataStore extends URIDataStore {
         public Object get(final Object key) {
             if (key instanceof String) {
                 switch ((String) key) {
-                    case XML.LOCALE:         return locale;
+                    case XML.LOCALE:         return getDataLocale();
                     case XML.TIMEZONE:       return timezone;
                     case XML.WARNING_FILTER: return this;
                 }
@@ -325,7 +300,7 @@ public abstract class StaxDataStore extends URIDataStore {
          */
         @Override
         public void report(String message, String errorType, Object info, 
Location location) {
-            final LogRecord record = new LogRecord(Level.WARNING, message);
+            final var record = new LogRecord(Level.WARNING, message);
             
record.setSourceClassName(StaxDataStore.this.getClass().getCanonicalName());
             // record.setLoggerName(…) will be invoked by `listeners` with 
inferred name.
             listeners.warning(record);
@@ -348,7 +323,7 @@ public abstract class StaxDataStore extends URIDataStore {
          */
         @Override
         public String toString() {
-            return Strings.toString(getClass(), "locale", locale, "timezone", 
timezone);
+            return Strings.toString(getClass(), "dataLocale", dataLocale, 
"timezone", timezone);
         }
     }
 
@@ -374,20 +349,53 @@ public abstract class StaxDataStore extends URIDataStore {
         return provider.getShortName();
     }
 
+    /**
+     * Returns the character encoding of the file content, or {@code null} if 
unspecified.
+     * This is often (but not always) ignored at reading time, but taken in 
account at writing time.
+     */
+    final Charset getEncoding() {
+        return encoding;
+    }
+
+    /**
+     * Returns the locale to use for locale-sensitive data 
(<strong>not</strong> for logging or warning messages),
+     * or {@code null} if unspecified.
+     *
+     * @see OptionKey#LOCALE
+     */
+    final Locale getDataLocale() {
+        return dataLocale;
+    }
+
     /**
      * Returns the factory for StAX readers. The same instance is returned for 
all {@code StaxDataStore} lifetime.
      * Warnings emitted by readers created by this factory will be forwarded 
to the {@link #listeners}.
      *
      * <p>This method is indirectly invoked by {@link 
#createReader(StaxStreamReader)},
      * through a call to {@link InputType#create(StaxDataStore, Object)}.</p>
+     *
+     * <h4>Security</h4>
+     * Unless the user has configured the {@code javax.xml.accessExternalDTD} 
property to something else
+     * than {@code "all"}, this class disallows external DTDs referenced by 
the {@code "file"} protocols.
+     * Allowed protocols are <abbr>HTTP</abbr> and <abbr>HTTPS</abbr> (list 
may be expanded if needed).
+     *
+     * @see org.apache.sis.xml.internal.shared.InputFactory#FACTORY
      */
     final XMLInputFactory inputFactory() {
         assert Thread.holdsLock(this);
-        if (inputFactory == null) {
-            inputFactory = XMLInputFactory.newInstance();
-            inputFactory.setXMLReporter(configuration);
+        XMLInputFactory factory = inputFactory;
+        if (factory == null) {
+            factory = XMLInputFactory.newInstance();
+            if 
(factory.isPropertySupported(XMLConstants.FEATURE_SECURE_PROCESSING)) {
+                factory.setProperty(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
+            }
+            if 
("all".equals(factory.getProperty(XMLConstants.ACCESS_EXTERNAL_DTD))) {
+                factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, 
"http,https");
+            }
+            factory.setXMLReporter(configuration);
+            inputFactory = factory;     // Set only on success.
         }
-        return inputFactory;
+        return factory;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamWriter.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamWriter.java
index b83bac8828..53e21e4d77 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxStreamWriter.java
@@ -142,7 +142,7 @@ public abstract class StaxStreamWriter extends StaxStreamIO 
implements Consumer<
      *         {@link DataStoreException}, {@link ClassCastException}, 
<i>etc.</i>
      */
     public void writeStartDocument() throws Exception {
-        final Charset encoding = owner.encoding;
+        final Charset encoding = owner.getEncoding();
         if (encoding != null) {
             writer.writeStartDocument(encoding.name(), null);
         } else {
@@ -294,7 +294,7 @@ public abstract class StaxStreamWriter extends StaxStreamIO 
implements Consumer<
             m = getMarshallerPool().acquireMarshaller();
             m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
             m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.FALSE);    
 // Formatting will be done by FormattedWriter.
-            final Charset encoding = owner.encoding;
+            final Charset encoding = owner.getEncoding();
             if (encoding != null) {
                 m.setProperty(Marshaller.JAXB_ENCODING, encoding.name());
             }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
index 0870fff38a..e57ab44988 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.storage.base;
 
-import java.util.Map;
 import java.util.HashMap;
 import java.util.Arrays;
 import java.util.Optional;
@@ -28,12 +27,14 @@ import java.io.BufferedWriter;
 import java.io.BufferedInputStream;
 import java.io.InputStream;
 import java.io.IOException;
+import java.io.FileNotFoundException;
 import java.net.URL;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Files;
 import java.nio.file.StandardOpenOption;
+import java.nio.file.NoSuchFileException;
 import java.nio.charset.Charset;
 import javax.xml.transform.Source;
 import jakarta.xml.bind.JAXBException;
@@ -413,7 +414,7 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource {
             return false;
         };
         // Cannot use Map.of(…) because it does not accept null values.
-        Map<String,Object> properties = new HashMap<>(8);
+        final var properties = new HashMap<String, Object>(8);
         properties.put(XML.LOCALE, dataLocale);
         properties.put(XML.TIMEZONE, timezone);
         properties.put(XML.WARNING_FILTER, handler);


Reply via email to