You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/09/28 09:35:23 UTC

(cxf) branch coheigea/schema created (now 296f2b9ece2)

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

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


      at 296f2b9ece2 Harden schema and stylesheet loading against resolver bypasses

This branch includes the following new commits:

     new 296f2b9ece2 Harden schema and stylesheet loading against resolver bypasses

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(cxf) 01/01: Harden schema and stylesheet loading against resolver bypasses

Posted by co...@apache.org.
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 <co...@apache.org>
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();
+        }
+    }
+}