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

coheigea pushed a commit to branch coheigea/schema
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 296f2b9ece29337d9ac5a68c9e5d8c8217d95d85
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Sep 28 10:34:56 2026 +0100

    Harden schema and stylesheet loading against resolver bypasses
---
 .../java/org/apache/cxf/resource/URIResolver.java  |  16 ++
 .../cxf/ws/addressing/EndpointReferenceUtils.java  |  54 +++++-
 .../org/apache/cxf/resource/URIResolverTest.java   |  24 +++
 .../ws/addressing/EndpointReferenceUtilsTest.java  |  67 +++++++-
 .../cxf/jaxrs/provider/XSLTJaxbProvider.java       |  74 +++++++-
 .../cxf/jaxrs/utils/schemas/SchemaHandler.java     |  34 ++++
 .../provider/XSLTJaxbProviderResolverTest.java     | 187 +++++++++++++++++++++
 .../cxf/jaxrs/utils/schemas/SchemaHandlerTest.java | 178 ++++++++++++++++++++
 8 files changed, 622 insertions(+), 12 deletions(-)

diff --git a/core/src/main/java/org/apache/cxf/resource/URIResolver.java 
b/core/src/main/java/org/apache/cxf/resource/URIResolver.java
index 633cf682e96..301dab0f3d2 100644
--- a/core/src/main/java/org/apache/cxf/resource/URIResolver.java
+++ b/core/src/main/java/org/apache/cxf/resource/URIResolver.java
@@ -69,6 +69,10 @@ public class URIResolver implements AutoCloseable {
         Collections.unmodifiableSet(
             new HashSet<>(Arrays.asList("file", "http", "https", "jar", "zip", 
"wsjar", "local", "classpath", "vfs",
                     "resource")));
+    private static final Set<String> NETWORK_URL_SCHEMES =
+        Collections.unmodifiableSet(new HashSet<>(Arrays.asList("http", 
"https", "ftp")));
+    private static final Set<String> ADDITIONAL_LOCAL_URL_SCHEMES =
+        Collections.unmodifiableSet(new HashSet<>(Arrays.asList("bundle", 
"bundleresource")));
 
     private Map<String, LoadingByteArrayOutputStream> cache = new HashMap<>();
     private File file;
@@ -465,6 +469,18 @@ public class URIResolver implements AutoCloseable {
         }
     }
 
+    /**
+     * Returns the URL schemes that refer to local resources: the allowed 
schemes (including any configured
+     * with the "org.apache.cxf.resource.uriresolver.allowedSchemes" system 
property) plus the OSGi bundle
+     * schemes, minus the network schemes (http, https, ftp).
+     */
+    public static Set<String> getLocalSchemes() {
+        Set<String> local = getAllowedSchemes();
+        local.addAll(ADDITIONAL_LOCAL_URL_SCHEMES);
+        local.removeAll(NETWORK_URL_SCHEMES);
+        return local;
+    }
+
     public static Set<String> getAllowedSchemes() {
         Set<String> allowed = new HashSet<>(DEFAULT_ALLOWED_URL_SCHEMES);
         String configured = 
SystemPropertyAction.getPropertyOrNull(ALLOWED_URL_SCHEMES_PROPERTY);
diff --git 
a/core/src/main/java/org/apache/cxf/ws/addressing/EndpointReferenceUtils.java 
b/core/src/main/java/org/apache/cxf/ws/addressing/EndpointReferenceUtils.java
index 90bb8bc9230..b6604527d4d 100644
--- 
a/core/src/main/java/org/apache/cxf/ws/addressing/EndpointReferenceUtils.java
+++ 
b/core/src/main/java/org/apache/cxf/ws/addressing/EndpointReferenceUtils.java
@@ -20,6 +20,7 @@
 package org.apache.cxf.ws.addressing;
 
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.lang.ref.Reference;
 import java.lang.ref.SoftReference;
 import java.net.MalformedURLException;
@@ -39,6 +40,7 @@ import java.util.logging.Logger;
 
 import javax.xml.XMLConstants;
 import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
 import javax.xml.transform.Source;
 import javax.xml.transform.dom.DOMSource;
@@ -46,6 +48,7 @@ import javax.xml.transform.stream.StreamSource;
 import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
 
+import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 import org.w3c.dom.ls.LSInput;
@@ -67,7 +70,6 @@ import org.apache.cxf.common.xmlschema.LSInputImpl;
 import org.apache.cxf.endpoint.EndpointResolverRegistry;
 import org.apache.cxf.endpoint.Server;
 import org.apache.cxf.endpoint.ServerRegistry;
-import org.apache.cxf.helpers.IOUtils;
 import org.apache.cxf.helpers.LoadingByteArrayOutputStream;
 import org.apache.cxf.resource.ExtendedURIResolver;
 import org.apache.cxf.resource.ResourceManager;
@@ -110,6 +112,11 @@ public final class EndpointReferenceUtils {
 
         public LSInput resolveResource(String type, String namespaceURI, 
String publicId,
                                        String systemId, String baseURI) {
+            if (!XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(type)) {
+                // Leave DTDs and external entities to the SchemaFactory, so 
that the
+                // ACCESS_EXTERNAL_DTD restriction configured on it is enforced
+                return null;
+            }
 
             String newId = systemId;
             if (baseURI != null && systemId != null) {  //add additional 
systemId null check
@@ -211,11 +218,23 @@ public final class EndpointReferenceUtils {
                     InputSource source = AccessController.doPrivileged(
                         (PrivilegedAction<InputSource>) () -> 
resolver.resolve(sid, buri));
                     if (source != null) {
-                        impl = new LSInputImpl();
-                        impl.setByteStream(source.getByteStream());
-                        impl.setSystemId(source.getSystemId());
-                        impl.setPublicId(source.getPublicId());
-                        return impl;
+                        // The external access restrictions set on the 
SchemaFactory do not apply to
+                        // inputs returned from this resolver, so re-parse the 
resolved document with
+                        // the secure StAX parser to strip any DTD before it 
reaches the SchemaFactory.
+                        byte[] bytes = null;
+                        try (InputStream ins = source.getByteStream()) {
+                            if (ins != null) {
+                                bytes = toSecureBytes(ins, 
source.getSystemId());
+                            }
+                        } catch (Exception e) {
+                            LOG.log(Level.WARNING, "Could not parse Schema for 
" + systemId, e);
+                            return null;
+                        }
+                        if (bytes != null) {
+                            impl = createInput(source.getSystemId(), bytes);
+                            impl.setPublicId(source.getPublicId());
+                            return impl;
+                        }
                     }
                 }
                 LOG.warning("Could not resolve Schema for " + systemId);
@@ -555,7 +574,9 @@ public final class EndpointReferenceUtils {
                             if (resolver.getInputStream() == null) {
                                 sch.write(out);
                             } else {
-                                
IOUtils.copyAndCloseInput(resolver.getInputStream(), out);
+                                // Re-parse securely rather than copying the 
raw bytes, so that any
+                                // DTD in the fetched document never reaches 
the SchemaFactory
+                                
out.write(toSecureBytes(resolver.getInputStream(), sch.getSourceURI()));
                             }
                         } catch (Exception e) {
                             //ignore, we'll just use what we have.  (though
@@ -593,6 +614,25 @@ public final class EndpointReferenceUtils {
         return schema;
     }
 
+    /**
+     * Parse the document with the secure StAX parser (DTDs and external 
entities disabled) and
+     * re-serialize the document element, dropping any DOCTYPE declaration.
+     */
+    private static byte[] toSecureBytes(InputStream ins, String systemId) 
throws XMLStreamException {
+        InputSource inputSource = new InputSource(ins);
+        inputSource.setSystemId(systemId);
+        Document doc = StaxUtils.read(inputSource);
+        LoadingByteArrayOutputStream out = new LoadingByteArrayOutputStream();
+        XMLStreamWriter writer = StaxUtils.createXMLStreamWriter(out);
+        try {
+            StaxUtils.copy(doc.getDocumentElement(), writer);
+            writer.flush();
+        } finally {
+            StaxUtils.close(writer);
+        }
+        return out.toByteArray();
+    }
+
     public static Schema getSchema(ServiceInfo serviceInfo) {
         return getSchema(serviceInfo, null);
     }
diff --git a/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java 
b/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java
index 4de9f5cf00c..ac932c0c819 100644
--- a/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java
+++ b/core/src/test/java/org/apache/cxf/resource/URIResolverTest.java
@@ -25,6 +25,7 @@ import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.Set;
 
 import org.apache.cxf.helpers.IOUtils;
 
@@ -313,4 +314,27 @@ public class URIResolverTest {
             return super.findResource(name);
         }
     }
+
+    @Test
+    public void testGetLocalSchemes() {
+        String property = "org.apache.cxf.resource.uriresolver.allowedSchemes";
+        String oldValue = System.getProperty(property);
+        System.setProperty(property, "jndi,ftp");
+        try {
+            Set<String> local = URIResolver.getLocalSchemes();
+            assertTrue(local.contains("file"));
+            assertTrue(local.contains("jar"));
+            assertTrue(local.contains("bundle"));
+            assertTrue("Configured local scheme should be included", 
local.contains("jndi"));
+            assertFalse(local.contains("http"));
+            assertFalse(local.contains("https"));
+            assertFalse("Configured network scheme should be excluded", 
local.contains("ftp"));
+        } finally {
+            if (oldValue == null) {
+                System.clearProperty(property);
+            } else {
+                System.setProperty(property, oldValue);
+            }
+        }
+    }
 }
diff --git 
a/core/src/test/java/org/apache/cxf/ws/addressing/EndpointReferenceUtilsTest.java
 
b/core/src/test/java/org/apache/cxf/ws/addressing/EndpointReferenceUtilsTest.java
index 8c229a0a168..83435714a89 100644
--- 
a/core/src/test/java/org/apache/cxf/ws/addressing/EndpointReferenceUtilsTest.java
+++ 
b/core/src/test/java/org/apache/cxf/ws/addressing/EndpointReferenceUtilsTest.java
@@ -20,10 +20,12 @@
 package org.apache.cxf.ws.addressing;
 
 import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.OutputStream;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.xml.validation.Schema;
@@ -36,13 +38,20 @@ import org.apache.cxf.service.model.ServiceInfo;
 import org.apache.cxf.staxutils.StaxUtils;
 import org.apache.ws.commons.schema.XmlSchema;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 public class EndpointReferenceUtilsTest {
+    private static final String NAMESPACE = 
"urn:test:endpoint:reference:utils";
+    private static final String MEMORY_SYSTEM_ID = "memory:/schema.xsd";
+
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
 
     @Test
     public void testGetSchemaCanOpenSourceUriAllowedByDefaultSchemes() throws 
Exception {
@@ -75,8 +84,62 @@ public class EndpointReferenceUtilsTest {
         }
     }
 
+    @Test
+    public void testGetSchemaDoesNotResolveDtdFromSourceUri() throws Exception 
{
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            File xsd = writeSchemaWithExternalDtd("source.xsd", NAMESPACE, 
probeServer.getPort());
+            ServiceInfo serviceInfo = 
createServiceInfo(xsd.toURI().toString());
+
+            Schema schema = EndpointReferenceUtils.getSchema(serviceInfo, 
null);
+
+            probeServer.awaitCompletion();
+            assertFalse("External DTD of the sourceURI document should not be 
loaded", probeServer.wasConnected());
+            assertNotNull(schema);
+        }
+    }
+
+    @Test
+    public void testGetSchemaDoesNotResolveDtdFromImportedSchema() throws 
Exception {
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            String importedNamespace = 
"urn:test:endpoint:reference:utils:imported";
+            File importedXsd = writeSchemaWithExternalDtd("imported.xsd", 
importedNamespace, probeServer.getPort());
+
+            String schemaText =
+                "<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema' "
+                + "targetNamespace='" + NAMESPACE + "' 
elementFormDefault='qualified'>"
+                + "<xsd:import namespace='" + importedNamespace + "' "
+                + "schemaLocation='" + importedXsd.toURI().toString() + "'/>"
+                + "<xsd:element name='value' type='xsd:string'/>"
+                + "</xsd:schema>";
+            Document doc = StaxUtils.read(new 
ByteArrayInputStream(schemaText.getBytes(StandardCharsets.UTF_8)));
+
+            // Only the SchemaInfo element carries the import, so it is 
resolved by getSchema itself
+            ServiceInfo serviceInfo = createServiceInfo(MEMORY_SYSTEM_ID);
+            
serviceInfo.getSchemas().iterator().next().setElement(doc.getDocumentElement());
+
+            Schema schema = EndpointReferenceUtils.getSchema(serviceInfo, 
null);
+
+            probeServer.awaitCompletion();
+            assertFalse("External DTD of an imported schema should not be 
loaded", probeServer.wasConnected());
+            assertNotNull(schema);
+        }
+    }
+
+    private File writeSchemaWithExternalDtd(String name, String namespace, int 
port) throws Exception {
+        String schemaText =
+            "<?xml version='1.0'?>"
+            + "<!DOCTYPE xsd:schema SYSTEM 'http://127.0.0.1:"; + port + 
"/evil.dtd'>"
+            + "<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema' "
+            + "targetNamespace='" + namespace + "' 
elementFormDefault='qualified'>"
+            + "<xsd:element name='other' type='xsd:string'/>"
+            + "</xsd:schema>";
+        File file = tempFolder.newFile(name);
+        Files.write(file.toPath(), 
schemaText.getBytes(StandardCharsets.UTF_8));
+        return file;
+    }
+
     private ServiceInfo createServiceInfo(String sourceUri) throws Exception {
-        String namespace = "urn:test:endpoint:reference:utils";
+        String namespace = NAMESPACE;
         String schemaText =
             "<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema' "
             + "targetNamespace='" + namespace + "' 
elementFormDefault='qualified'>"
@@ -90,7 +153,7 @@ public class EndpointReferenceUtilsTest {
 
         SchemaInfo schemaInfo = new SchemaInfo(namespace);
         schemaInfo.setSchema(xmlSchema);
-        schemaInfo.setSystemId("memory:/schema.xsd");
+        schemaInfo.setSystemId(MEMORY_SYSTEM_ID);
         serviceInfo.addSchema(schemaInfo);
 
         return serviceInfo;
diff --git 
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProvider.java
 
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProvider.java
index bec9e9c02cc..caea1f49636 100644
--- 
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProvider.java
+++ 
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProvider.java
@@ -25,12 +25,18 @@ import java.io.OutputStream;
 import java.io.Reader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -400,10 +406,22 @@ public class XSLTJaxbProvider<T> extends 
JAXBElementProvider<T> {
     public void setResolver(URIResolver resolver) {
         uriResolver = resolver;
         if (factory != null) {
-            factory.setURIResolver(uriResolver);
+            factory.setURIResolver(getEffectiveResolver());
         }
     }
 
+    /**
+     * The external access restrictions enabled by secure processing do not 
apply to sources returned
+     * by a custom URIResolver, so when secure processing is enabled the 
resolver is only consulted for
+     * local resources. Other references are left to the TransformerFactory, 
which rejects them.
+     */
+    private URIResolver getEffectiveResolver() {
+        if (uriResolver == null || !secureProcessing) {
+            return uriResolver;
+        }
+        return new LocalURIResolver(uriResolver);
+    }
+
     public void setSystemId(String system) {
         systemId = system;
     }
@@ -451,7 +469,7 @@ public class XSLTJaxbProvider<T> extends 
JAXBElementProvider<T> {
             throw ExceptionUtils.toInternalServerErrorException(null, null);
         }
 
-        TemplatesImpl templ = new TemplatesImpl(templates, uriResolver);
+        TemplatesImpl templ = new TemplatesImpl(templates, 
getEffectiveResolver());
         MessageContext mc = getContext();
         if (mc != null) {
             UriInfo ui = mc.getUriInfo();
@@ -525,7 +543,7 @@ public class XSLTJaxbProvider<T> extends 
JAXBElementProvider<T> {
                 factory = 
(SAXTransformerFactory)TransformerFactory.newInstance();
                 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
secureProcessing);
                 if (uriResolver != null) {
-                    factory.setURIResolver(uriResolver);
+                    factory.setURIResolver(getEffectiveResolver());
                 }
             }
             return factory.newTemplates(source);
@@ -544,6 +562,56 @@ public class XSLTJaxbProvider<T> extends 
JAXBElementProvider<T> {
         this.secureProcessing = secureProcessing;
     }
 
+    private static final class LocalURIResolver implements URIResolver {
+        private static final Set<String> ARCHIVE_SCHEMES = new 
HashSet<>(Arrays.asList("jar", "wsjar", "zip"));
+
+        private final URIResolver delegate;
+
+        LocalURIResolver(URIResolver delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public Source resolve(String href, String base) throws 
TransformerException {
+            String scheme = getScheme(href, base);
+            if (scheme == null || 
org.apache.cxf.resource.URIResolver.getLocalSchemes().contains(scheme)) {
+                return delegate.resolve(href, base);
+            }
+            LOG.warning("Resolving " + href + " is not allowed with secure 
processing enabled");
+            return null;
+        }
+
+        private static String getScheme(String href, String base) {
+            try {
+                URI uri = new URI(href);
+                if (uri.getScheme() == null) {
+                    // A relative reference is resolved against the base URI
+                    if (base == null) {
+                        return null;
+                    }
+                    uri = new URI(base);
+                }
+                return getInnermostScheme(uri);
+            } catch (URISyntaxException ex) {
+                // Not a URI, leave it to the TransformerFactory
+                return "";
+            }
+        }
+
+        private static String getInnermostScheme(URI uri) throws 
URISyntaxException {
+            String scheme = uri.getScheme();
+            if (scheme == null) {
+                return null;
+            }
+            scheme = scheme.toLowerCase(Locale.ROOT);
+            if (ARCHIVE_SCHEMES.contains(scheme)) {
+                // An archive URL such as jar:http://host/a.jar!/a.xsl is only 
as local as the archive
+                return getInnermostScheme(new 
URI(uri.getRawSchemeSpecificPart()));
+            }
+            return scheme;
+        }
+    }
+
     private static class TemplatesImpl implements Templates {
 
         private Templates templates;
diff --git 
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandler.java
 
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandler.java
index a8f4bf17b9f..85843ef7ccb 100644
--- 
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandler.java
+++ 
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandler.java
@@ -29,7 +29,10 @@ import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
+import javax.xml.XMLConstants;
 import javax.xml.transform.Source;
 import javax.xml.transform.stream.StreamSource;
 import javax.xml.validation.Schema;
@@ -38,18 +41,25 @@ import javax.xml.validation.SchemaFactory;
 import org.w3c.dom.ls.LSInput;
 import org.w3c.dom.ls.LSResourceResolver;
 
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+
 import org.apache.cxf.Bus;
 import org.apache.cxf.BusFactory;
 import org.apache.cxf.catalog.OASISCatalogManager;
+import org.apache.cxf.common.logging.LogUtils;
 import org.apache.cxf.common.util.ClasspathScanner;
 import org.apache.cxf.common.xmlschema.LSInputImpl;
 import org.apache.cxf.jaxrs.utils.ResourceUtils;
+import org.apache.cxf.resource.URIResolver;
 import org.apache.ws.commons.schema.constants.Constants;
 
 public class SchemaHandler {
 
     static final String DEFAULT_CATALOG_LOCATION = 
"classpath:META-INF/jax-rs-catalog.xml";
 
+    private static final Logger LOG = LogUtils.getLogger(SchemaHandler.class);
+
     private Schema schema;
     private Bus bus;
     private String catalogLocation;
@@ -83,6 +93,7 @@ public class SchemaHandler {
     public static Schema createSchema(List<String> locations, String 
catalogLocation, final Bus bus) {
 
         SchemaFactory factory = 
SchemaFactory.newInstance(Constants.URI_2001_SCHEMA_XSD);
+        setSecureProperties(factory);
         try {
             List<Source> sources = new ArrayList<>();
             for (String loc : locations) {
@@ -154,4 +165,27 @@ public class SchemaHandler {
         }
     }
 
+    /**
+     * Disallow external DTDs and restrict schema imports/includes to local 
resources. Remote
+     * schemas can still be used by mapping them to local copies with an OASIS 
catalog.
+     */
+    private static void setSecureProperties(SchemaFactory factory) {
+        try {
+            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
+        } catch (SAXNotRecognizedException | SAXNotSupportedException e) {
+            LOG.log(Level.WARNING, "The feature '" + 
XMLConstants.FEATURE_SECURE_PROCESSING
+                + "' is not supported.");
+        }
+        try {
+            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        } catch (SAXNotRecognizedException | SAXNotSupportedException e) {
+            LOG.log(Level.WARNING, "The property '" + 
XMLConstants.ACCESS_EXTERNAL_DTD + "' is not supported.");
+        }
+        try {
+            factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, 
String.join(",", URIResolver.getLocalSchemes()));
+        } catch (SAXNotRecognizedException | SAXNotSupportedException e) {
+            LOG.log(Level.WARNING, "The property '" + 
XMLConstants.ACCESS_EXTERNAL_SCHEMA + "' is not supported.");
+        }
+    }
+
 }
diff --git 
a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProviderResolverTest.java
 
b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProviderResolverTest.java
new file mode 100644
index 00000000000..37a69652ae5
--- /dev/null
+++ 
b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/XSLTJaxbProviderResolverTest.java
@@ -0,0 +1,187 @@
+/**
+ * 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.cxf.jaxrs.provider;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.io.StringReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.Templates;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class XSLTJaxbProviderResolverTest {
+
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    @Test
+    public void testResolverIsUsedForLocalImport() throws Exception {
+        writeFile("imported.xsl", stylesheet(""));
+        File main = writeFile("main.xsl", stylesheet("<xsl:import 
href='imported.xsl'/>"));
+        RecordingResolver resolver = new RecordingResolver();
+
+        Templates templates = createTemplates(main.toURI().toURL(), resolver, 
true);
+
+        assertNotNull(templates);
+        assertTrue("Resolver should be used for a local import", 
resolver.wasCalled());
+    }
+
+    @Test
+    public void testResolverIsNotUsedForRemoteImport() throws Exception {
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            File main = writeFile("main.xsl", stylesheet(
+                "<xsl:import href='http://127.0.0.1:"; + probeServer.getPort() 
+ "/imported.xsl'/>"));
+            RecordingResolver resolver = new RecordingResolver();
+
+            Templates templates = createTemplates(main.toURI().toURL(), 
resolver, true);
+
+            probeServer.awaitCompletion();
+            assertFalse("Remote import should not be loaded", 
probeServer.wasConnected());
+            assertFalse("Resolver should not be used for a remote import", 
resolver.wasCalled());
+            assertNull(templates);
+        }
+    }
+
+    @Test
+    public void testResolverIsNotUsedForRemoteArchiveImport() throws Exception 
{
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            File main = writeFile("main.xsl", stylesheet(
+                "<xsl:import href='jar:http://127.0.0.1:"; + 
probeServer.getPort() + "/a.jar!/imported.xsl'/>"));
+            RecordingResolver resolver = new RecordingResolver();
+
+            Templates templates = createTemplates(main.toURI().toURL(), 
resolver, true);
+
+            probeServer.awaitCompletion();
+            assertFalse("Remote archive import should not be loaded", 
probeServer.wasConnected());
+            assertFalse("Resolver should not be used for a remote archive 
import", resolver.wasCalled());
+            assertNull(templates);
+        }
+    }
+
+    @Test
+    public void testResolverIsUsedForRemoteImportWithoutSecureProcessing() 
throws Exception {
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            File main = writeFile("main.xsl", stylesheet(
+                "<xsl:import href='http://127.0.0.1:"; + probeServer.getPort() 
+ "/imported.xsl'/>"));
+            RecordingResolver resolver = new RecordingResolver();
+
+            createTemplates(main.toURI().toURL(), resolver, false);
+
+            assertTrue("Resolver should be used when secure processing is 
disabled", resolver.wasCalled());
+        }
+    }
+
+    private static Templates createTemplates(URL url, URIResolver resolver, 
boolean secureProcessing) {
+        XSLTJaxbProvider<Object> provider = new XSLTJaxbProvider<>();
+        provider.setSecureProcessing(secureProcessing);
+        provider.setResolver(resolver);
+        return provider.createTemplates(url);
+    }
+
+    private static String stylesheet(String imports) {
+        return "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
+            + imports
+            + "<xsl:template match='/'><result/></xsl:template>"
+            + "</xsl:stylesheet>";
+    }
+
+    private File writeFile(String name, String content) throws Exception {
+        File file = tempFolder.newFile(name);
+        Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
+        return file;
+    }
+
+    /**
+     * Records that it was called and returns an in-memory stylesheet without 
fetching anything.
+     */
+    private static final class RecordingResolver implements URIResolver {
+        private final AtomicBoolean called = new AtomicBoolean();
+
+        @Override
+        public Source resolve(String href, String base) {
+            called.set(true);
+            return new StreamSource(new StringReader(stylesheet("")), href);
+        }
+
+        boolean wasCalled() {
+            return called.get();
+        }
+    }
+
+    private static final class LocalHttpProbeServer implements AutoCloseable {
+        private final ServerSocket serverSocket;
+        private final AtomicBoolean connected = new AtomicBoolean();
+        private final Thread thread;
+
+        private LocalHttpProbeServer() throws Exception {
+            this.serverSocket = new ServerSocket(0);
+            this.serverSocket.setSoTimeout(750);
+            this.thread = new Thread(this::acceptOneConnection, 
"XSLTJaxbProviderResolverTest-HttpProbe");
+            this.thread.setDaemon(true);
+            this.thread.start();
+        }
+
+        private void acceptOneConnection() {
+            try (Socket socket = serverSocket.accept()) {
+                connected.set(true);
+                OutputStream out = socket.getOutputStream();
+                out.write("HTTP/1.1 404 Not Found\r\nContent-Length: 
0\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
+                out.flush();
+            } catch (Exception ex) {
+                // timeout/no connection is expected for this test
+            }
+        }
+
+        private int getPort() {
+            return serverSocket.getLocalPort();
+        }
+
+        private boolean wasConnected() {
+            return connected.get();
+        }
+
+        private void awaitCompletion() throws InterruptedException {
+            thread.join(1500);
+        }
+
+        @Override
+        public void close() throws Exception {
+            serverSocket.close();
+            awaitCompletion();
+        }
+    }
+}
diff --git 
a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandlerTest.java
 
b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandlerTest.java
new file mode 100644
index 00000000000..9642d87ae5f
--- /dev/null
+++ 
b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/schemas/SchemaHandlerTest.java
@@ -0,0 +1,178 @@
+/**
+ * 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.cxf.jaxrs.utils.schemas;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import javax.xml.validation.Schema;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class SchemaHandlerTest {
+    private static final String NAMESPACE = "urn:test:schema:handler";
+    private static final String IMPORTED_NAMESPACE = 
"urn:test:schema:handler:imported";
+
+    @Rule
+    public TemporaryFolder tempFolder = new TemporaryFolder();
+
+    @Test
+    public void testRelativeImportFromFile() throws Exception {
+        writeFile("imported.xsd", importedSchema(""));
+        File main = writeFile("main.xsd", mainSchema("imported.xsd"));
+
+        Schema schema = SchemaHandler.createSchema(
+            Collections.singletonList(main.toURI().toString()), null, null);
+        assertNotNull(schema);
+    }
+
+    @Test
+    public void testRelativeImportFromJar() throws Exception {
+        File jar = tempFolder.newFile("schemas.jar");
+        try (JarOutputStream jos = new 
JarOutputStream(Files.newOutputStream(jar.toPath()))) {
+            jos.putNextEntry(new ZipEntry("schemas/main.xsd"));
+            
jos.write(mainSchema("imported.xsd").getBytes(StandardCharsets.UTF_8));
+            jos.closeEntry();
+            jos.putNextEntry(new ZipEntry("schemas/imported.xsd"));
+            jos.write(importedSchema("").getBytes(StandardCharsets.UTF_8));
+            jos.closeEntry();
+        }
+
+        Schema schema = SchemaHandler.createSchema(
+            Collections.singletonList("jar:" + jar.toURI() + 
"!/schemas/main.xsd"), null, null);
+        assertNotNull(schema);
+    }
+
+    @Test
+    public void testExternalDtdIsNotLoaded() throws Exception {
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            String doctype = "<!DOCTYPE xsd:schema SYSTEM 'http://127.0.0.1:"; 
+ probeServer.getPort()
+                + "/evil.dtd'>";
+            File main = writeFile("main.xsd", importedSchema(doctype));
+
+            try {
+                
SchemaHandler.createSchema(Collections.singletonList(main.toURI().toString()), 
null, null);
+                fail("Schema with an external DTD should not be loaded");
+            } catch (IllegalArgumentException ex) {
+                // expected
+            }
+
+            probeServer.awaitCompletion();
+            assertFalse("External DTD should not be loaded", 
probeServer.wasConnected());
+        }
+    }
+
+    @Test
+    public void testRemoteImportIsNotLoaded() throws Exception {
+        try (LocalHttpProbeServer probeServer = new LocalHttpProbeServer()) {
+            File main = writeFile("main.xsd",
+                mainSchema("http://127.0.0.1:"; + probeServer.getPort() + 
"/imported.xsd"));
+
+            try {
+                
SchemaHandler.createSchema(Collections.singletonList(main.toURI().toString()), 
null, null);
+                fail("Schema importing a remote schema should not be loaded");
+            } catch (IllegalArgumentException ex) {
+                // expected
+            }
+
+            probeServer.awaitCompletion();
+            assertFalse("Remote schema import should not be loaded", 
probeServer.wasConnected());
+        }
+    }
+
+    private static String mainSchema(String importLocation) {
+        return "<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema' 
xmlns:i='" + IMPORTED_NAMESPACE + "' "
+            + "targetNamespace='" + NAMESPACE + "' 
elementFormDefault='qualified'>"
+            + "<xsd:import namespace='" + IMPORTED_NAMESPACE + "' 
schemaLocation='" + importLocation + "'/>"
+            + "<xsd:element name='value' type='i:valueType'/>"
+            + "</xsd:schema>";
+    }
+
+    private static String importedSchema(String doctype) {
+        return "<?xml version='1.0'?>" + doctype
+            + "<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema' "
+            + "targetNamespace='" + IMPORTED_NAMESPACE + "'>"
+            + "<xsd:simpleType name='valueType'><xsd:restriction 
base='xsd:string'/></xsd:simpleType>"
+            + "</xsd:schema>";
+    }
+
+    private File writeFile(String name, String content) throws Exception {
+        File file = tempFolder.newFile(name);
+        Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
+        return file;
+    }
+
+    private static final class LocalHttpProbeServer implements AutoCloseable {
+        private final ServerSocket serverSocket;
+        private final AtomicBoolean connected = new AtomicBoolean();
+        private final Thread thread;
+
+        private LocalHttpProbeServer() throws Exception {
+            this.serverSocket = new ServerSocket(0);
+            this.serverSocket.setSoTimeout(750);
+            this.thread = new Thread(this::acceptOneConnection, 
"SchemaHandlerTest-HttpProbe");
+            this.thread.setDaemon(true);
+            this.thread.start();
+        }
+
+        private void acceptOneConnection() {
+            try (Socket socket = serverSocket.accept()) {
+                connected.set(true);
+                OutputStream out = socket.getOutputStream();
+                out.write("HTTP/1.1 404 Not Found\r\nContent-Length: 
0\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
+                out.flush();
+            } catch (Exception ex) {
+                // timeout/no connection is expected for this test
+            }
+        }
+
+        private int getPort() {
+            return serverSocket.getLocalPort();
+        }
+
+        private boolean wasConnected() {
+            return connected.get();
+        }
+
+        private void awaitCompletion() throws InterruptedException {
+            thread.join(1500);
+        }
+
+        @Override
+        public void close() throws Exception {
+            serverSocket.close();
+            awaitCompletion();
+        }
+    }
+}

Reply via email to