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(); + } + } +}
