From 0ab81e4f8f3d7e380b5e9718912f70c3bd0fac87 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 24 Jul 2017 16:21:08 +0100 Subject: [PATCH] Prevent loader classes from being served by executable war Closes gh-5550 --- ...ContainerWarPackagingIntegrationTests.java | 11 ++ .../jetty/JettyServletWebServerFactory.java | 100 +++++++++++++++- .../tomcat/TomcatServletWebServerFactory.java | 110 +++++++++++++++++- .../UndertowServletWebServerFactory.java | 59 +++++++--- 4 files changed, 262 insertions(+), 18 deletions(-) diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java index ed9d2d7fc4..78c4dd0e3e 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java @@ -86,4 +86,15 @@ public class EmbeddedServletContainerWarPackagingIntegrationTests assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void loaderClassesAreNotAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest.getForEntity( + "/org/springframework/boot/loader/Launcher.class", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + entity = this.rest.getForEntity( + "/org/springframework/../springframework/boot/loader/Launcher.class", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 8cb8b062e7..1129d3a580 100644 --- a/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -18,8 +18,11 @@ package org.springframework.boot.web.embedded.jetty; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; +import java.net.MalformedURLException; import java.net.URL; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -389,12 +392,14 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor private void configureDocumentRoot(WebAppContext handler) { File root = getValidDocumentRoot(); - root = (root != null ? root : createTempDir("jetty-docbase")); + File docBase = (root != null ? root : createTempDir("jetty-docbase")); try { List resources = new ArrayList<>(); + Resource rootResource = docBase.isDirectory() + ? Resource.newResource(docBase.getCanonicalFile()) + : JarResource.newJarResource(Resource.newResource(docBase)); resources.add( - root.isDirectory() ? Resource.newResource(root.getCanonicalFile()) - : JarResource.newJarResource(Resource.newResource(root))); + root == null ? rootResource : new LoaderHidingResource(rootResource)); for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) { Resource resource = createResource(resourceJarUrl); // Jetty 9.2 and earlier do not support nested jars. See @@ -719,4 +724,93 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor } + private static final class LoaderHidingResource extends Resource { + + private final Resource delegate; + + private LoaderHidingResource(Resource delegate) { + this.delegate = delegate; + } + + @Override + public Resource addPath(String path) throws IOException, MalformedURLException { + if (path.startsWith("/org/springframework/boot")) { + return null; + } + return this.delegate.addPath(path); + } + + @Override + public boolean isContainedIn(Resource resource) throws MalformedURLException { + return this.delegate.isContainedIn(resource); + } + + @Override + public void close() { + close(); + } + + @Override + public boolean exists() { + return this.delegate.exists(); + } + + @Override + public boolean isDirectory() { + return this.delegate.isDirectory(); + } + + @Override + public long lastModified() { + return this.delegate.lastModified(); + } + + @Override + public long length() { + return this.delegate.length(); + } + + @Override + @Deprecated + public URL getURL() { + return this.delegate.getURL(); + } + + @Override + public File getFile() throws IOException { + return this.delegate.getFile(); + } + + @Override + public String getName() { + return this.delegate.getName(); + } + + @Override + public InputStream getInputStream() throws IOException { + return this.delegate.getInputStream(); + } + + @Override + public ReadableByteChannel getReadableByteChannel() throws IOException { + return this.delegate.getReadableByteChannel(); + } + + @Override + public boolean delete() throws SecurityException { + return this.delegate.delete(); + } + + @Override + public boolean renameTo(Resource dest) throws SecurityException { + return this.delegate.renameTo(dest); + } + + @Override + public String[] list() { + return this.delegate.list(); + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index aa245a36e2..d888ab2c33 100644 --- a/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -18,6 +18,8 @@ package org.springframework.boot.web.embedded.tomcat; import java.io.File; import java.io.FileNotFoundException; +import java.io.InputStream; +import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; @@ -39,16 +41,23 @@ import org.apache.catalina.Engine; import org.apache.catalina.Host; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Manager; import org.apache.catalina.Valve; +import org.apache.catalina.WebResource; import org.apache.catalina.WebResourceRoot.ResourceSetType; +import org.apache.catalina.WebResourceSet; import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.loader.WebappLoader; import org.apache.catalina.session.StandardManager; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat.FixContextListener; +import org.apache.catalina.util.LifecycleBase; +import org.apache.catalina.webresources.AbstractResourceSet; +import org.apache.catalina.webresources.EmptyResource; +import org.apache.catalina.webresources.StandardRoot; import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; @@ -71,6 +80,7 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -183,12 +193,16 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto } protected void prepareContext(Host host, ServletContextInitializer[] initializers) { - File docBase = getValidDocumentRoot(); - docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase")); + File documentRoot = getValidDocumentRoot(); final TomcatEmbeddedContext context = new TomcatEmbeddedContext(); + if (documentRoot != null) { + context.setResources(new LoaderHidingResourceRoot(context)); + } context.setName(getContextPath()); context.setDisplayName(getDisplayName()); context.setPath(getContextPath()); + File docBase = (documentRoot != null ? documentRoot + : createTempDir("tomcat-docbase")); context.setDocBase(docBase.getAbsolutePath()); context.addLifecycleListener(new FixContextListener()); context.setParentClassLoader( @@ -839,4 +853,96 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto } + private static final class LoaderHidingResourceRoot extends StandardRoot { + + private LoaderHidingResourceRoot(TomcatEmbeddedContext context) { + super(context); + } + + @Override + protected WebResourceSet createMainResourceSet() { + return new LoaderHidingWebResourceSet(super.createMainResourceSet()); + } + + } + + private static final class LoaderHidingWebResourceSet extends AbstractResourceSet { + + private final WebResourceSet delegate; + + private final Method initInternal; + + private LoaderHidingWebResourceSet(WebResourceSet delegate) { + this.delegate = delegate; + try { + this.initInternal = LifecycleBase.class.getDeclaredMethod("initInternal"); + this.initInternal.setAccessible(true); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public WebResource getResource(String path) { + if (path.startsWith("/org/springframework/boot")) { + return new EmptyResource(getRoot(), path); + } + return this.delegate.getResource(path); + } + + @Override + public String[] list(String path) { + return this.delegate.list(path); + } + + @Override + public Set listWebAppPaths(String path) { + return this.delegate.listWebAppPaths(path); + } + + @Override + public boolean mkdir(String path) { + return this.delegate.mkdir(path); + } + + @Override + public boolean write(String path, InputStream is, boolean overwrite) { + return this.delegate.write(path, is, overwrite); + } + + @Override + public URL getBaseUrl() { + return this.delegate.getBaseUrl(); + } + + @Override + public void setReadOnly(boolean readOnly) { + this.delegate.setReadOnly(readOnly); + } + + @Override + public boolean isReadOnly() { + return this.delegate.isReadOnly(); + } + + @Override + public void gc() { + this.delegate.gc(); + } + + @Override + protected void initInternal() throws LifecycleException { + if (this.delegate instanceof LifecycleBase) { + try { + ReflectionUtils.invokeMethod(this.initInternal, this.delegate); + } + catch (Exception ex) { + throw new LifecycleException(ex); + } + } + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 61c2cd3f78..f223912c91 100644 --- a/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -486,13 +486,15 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac } private ResourceManager getDocumentRootResourceManager() { - File root = getCanonicalDocumentRoot(); + File root = getValidDocumentRoot(); + File docBase = getCanonicalDocumentRoot(root); List metaInfResourceUrls = getUrlsOfJarsWithMetaInfResources(); List resourceJarUrls = new ArrayList(); List resourceManagers = new ArrayList(); - ResourceManager rootResourceManager = root.isDirectory() - ? new FileResourceManager(root, 0) : new JarResourceManager(root); - resourceManagers.add(rootResourceManager); + ResourceManager rootResourceManager = docBase.isDirectory() + ? new FileResourceManager(docBase, 0) : new JarResourceManager(docBase); + resourceManagers.add(root == null ? rootResourceManager + : new LoaderHidingResourceManager(rootResourceManager)); for (URL url : metaInfResourceUrls) { if ("file".equals(url.getProtocol())) { File file = new File(url.getFile()); @@ -518,16 +520,9 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac resourceManagers.toArray(new ResourceManager[resourceManagers.size()])); } - /** - * Return the document root in canonical form. Undertow uses File#getCanonicalFile() - * to determine whether a resource has been requested using the proper case but on - * Windows {@code java.io.tmpdir} may be set as a tilde-compressed pathname. - * @return the canonical document root - */ - private File getCanonicalDocumentRoot() { + private File getCanonicalDocumentRoot(File docBase) { try { - File root = getValidDocumentRoot(); - root = (root != null ? root : createTempDir("undertow-docbase")); + File root = docBase != null ? docBase : createTempDir("undertow-docbase"); return root.getCanonicalFile(); } catch (IOException e) { @@ -803,4 +798,42 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac } + private static final class LoaderHidingResourceManager implements ResourceManager { + + private final ResourceManager delegate; + + private LoaderHidingResourceManager(ResourceManager delegate) { + this.delegate = delegate; + } + + @Override + public Resource getResource(String path) throws IOException { + if (path.startsWith("/org/springframework/boot")) { + return null; + } + return this.delegate.getResource(path); + } + + @Override + public boolean isResourceChangeListenerSupported() { + return this.delegate.isResourceChangeListenerSupported(); + } + + @Override + public void registerResourceChangeListener(ResourceChangeListener listener) { + this.delegate.registerResourceChangeListener(listener); + } + + @Override + public void removeResourceChangeListener(ResourceChangeListener listener) { + this.delegate.removeResourceChangeListener(listener); + } + + @Override + public void close() throws IOException { + this.delegate.close(); + } + + } + }