This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 94f8b13c3b9 Harden schema and stylesheet loading against resolver
bypasses (#3512)
94f8b13c3b9 is described below
commit 94f8b13c3b9df125fa26e8ab937b3c80efef1af4
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Sep 28 15:22:56 2026 +0100
Harden schema and stylesheet loading against resolver bypasses (#3512)
* Harden schema and stylesheet loading against resolver bypasses
* Review comment
---
.../java/org/apache/cxf/resource/URIResolver.java | 55 ++++++
.../cxf/ws/addressing/EndpointReferenceUtils.java | 54 +++++-
.../org/apache/cxf/resource/URIResolverTest.java | 44 +++++
.../ws/addressing/EndpointReferenceUtilsTest.java | 67 +++++++-
.../cxf/jaxrs/provider/XSLTJaxbProvider.java | 35 +++-
.../cxf/jaxrs/utils/schemas/SchemaHandler.java | 34 ++++
.../provider/XSLTJaxbProviderResolverTest.java | 187 +++++++++++++++++++++
.../cxf/jaxrs/utils/schemas/SchemaHandlerTest.java | 178 ++++++++++++++++++++
8 files changed, 642 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..3828e95ab9c 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,12 @@ 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> ARCHIVE_URL_SCHEMES =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList("jar",
"wsjar", "zip")));
+ 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 +471,55 @@ 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;
+ }
+
+ /**
+ * Returns whether a reference refers to a local resource, i.e. whether
its scheme is one of
+ * {@link #getLocalSchemes()}. A relative reference is checked against the
scheme of the base URI, or is
+ * treated as local if there is no base URI. An archive URL such as
jar:http://host/a.jar!/a.xsl is checked
+ * against the scheme of the archive location. A reference that is not a
valid URI is not local.
+ */
+ public static boolean isLocalReference(String href, String base) {
+ if (href == null) {
+ return false;
+ }
+ try {
+ URI uri = new URI(href);
+ if (uri.getScheme() == null) {
+ if (base == null) {
+ return true;
+ }
+ uri = new URI(base);
+ }
+ String scheme = getInnermostScheme(uri);
+ return scheme == null || getLocalSchemes().contains(scheme);
+ } catch (URISyntaxException ex) {
+ return false;
+ }
+ }
+
+ private static String getInnermostScheme(URI uri) throws
URISyntaxException {
+ String scheme = uri.getScheme();
+ if (scheme == null) {
+ return null;
+ }
+ scheme = scheme.toLowerCase(Locale.ROOT);
+ if (ARCHIVE_URL_SCHEMES.contains(scheme)) {
+ return getInnermostScheme(new URI(uri.getRawSchemeSpecificPart()));
+ }
+ return scheme;
+ }
+
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..9cf74f079d2 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,47 @@ 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);
+ }
+ }
+ }
+
+ @Test
+ public void testIsLocalReference() {
+ assertTrue(URIResolver.isLocalReference("file:/tmp/a.xsl", null));
+ assertTrue(URIResolver.isLocalReference("jar:file:/tmp/a.jar!/a.xsl",
null));
+ assertTrue("Relative reference against a local base",
+ URIResolver.isLocalReference("b.xsl", "file:/tmp/a.xsl"));
+ assertTrue("Relative reference without a base",
URIResolver.isLocalReference("b.xsl", null));
+
+ assertFalse(URIResolver.isLocalReference("http://localhost/a.xsl",
null));
+ assertFalse(URIResolver.isLocalReference("HTTPS://localhost/a.xsl",
null));
+ assertFalse("Relative reference against a remote base",
+ URIResolver.isLocalReference("b.xsl", "http://localhost/a.xsl"));
+ assertFalse("Archive at a remote location",
+ URIResolver.isLocalReference("jar:http://localhost/a.jar!/a.xsl",
null));
+ assertFalse("Nested archive at a remote location",
+
URIResolver.isLocalReference("jar:jar:http://localhost/a.jar!/b.jar!/a.xsl",
null));
+ assertFalse("Invalid URI", URIResolver.isLocalReference("file:/tmp/a
b.xsl", null));
+ assertFalse(URIResolver.isLocalReference(null, null));
+ }
}
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..66fafc8a39e 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
@@ -400,10 +400,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 +463,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 +537,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 +556,23 @@ public class XSLTJaxbProvider<T> extends
JAXBElementProvider<T> {
this.secureProcessing = secureProcessing;
}
+ private static final class LocalURIResolver implements URIResolver {
+ private final URIResolver delegate;
+
+ LocalURIResolver(URIResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Source resolve(String href, String base) throws
TransformerException {
+ if (org.apache.cxf.resource.URIResolver.isLocalReference(href,
base)) {
+ return delegate.resolve(href, base);
+ }
+ LOG.warning("Resolving " + href + " is not allowed with secure
processing enabled");
+ return null;
+ }
+ }
+
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();
+ }
+ }
+}