diff options
author | Anna Koskinen <Ansku@users.noreply.github.com> | 2021-06-23 15:29:23 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-23 15:29:23 +0300 |
commit | f70bc4269c264214f0ab8ac637df877ded55bddf (patch) | |
tree | b68724842f1e9171b8e358f84c7853759d7c1df6 /server/src/test/java/com | |
parent | ba02350206ef25f6c29618f3cf7458f43543f3e8 (diff) | |
download | vaadin-framework-f70bc4269c264214f0ab8ac637df877ded55bddf.tar.gz vaadin-framework-f70bc4269c264214f0ab8ac637df877ded55bddf.zip |
fix: don't serve directories as static files (#12325)
Also prevents opening FileSystem for unknown schemes.
Modified cherry-picks of https://github.com/vaadin/flow/pull/11072 ,
https://github.com/vaadin/flow/pull/11147 , and
https://github.com/vaadin/flow/pull/11235
Diffstat (limited to 'server/src/test/java/com')
-rw-r--r-- | server/src/test/java/com/vaadin/server/VaadinServletTest.java | 447 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java | 196 |
2 files changed, 642 insertions, 1 deletions
diff --git a/server/src/test/java/com/vaadin/server/VaadinServletTest.java b/server/src/test/java/com/vaadin/server/VaadinServletTest.java index 652dc30665..006872b212 100644 --- a/server/src/test/java/com/vaadin/server/VaadinServletTest.java +++ b/server/src/test/java/com/vaadin/server/VaadinServletTest.java @@ -1,15 +1,54 @@ package com.vaadin.server; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import org.junit.After; +import org.junit.Assert; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; +/** + * Tests for {@link VaadinServlet}. + * <p> + * NOTE: some of these tests are not thread safe. Add + * {@code @net.jcip.annotations.NotThreadSafe} to this class if this module is + * converted to use {@code maven-failsafe-plugin} for parallelization. + */ public class VaadinServletTest { + @After + public void tearDown() { + Assert.assertNull(VaadinService.getCurrent()); + } + @Test public void testGetLastPathParameter() { assertEquals("", @@ -111,13 +150,419 @@ public class VaadinServletTest { } + @Test + @SuppressWarnings("deprecation") + public void directoryIsNotResourceRequest() throws Exception { + VaadinServlet servlet = new VaadinServlet(); + servlet.init(new MockServletConfig()); + // this request isn't actually used for anything within the + // isAllowedVAADINResourceUrl calls, no need to configure it + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + + TemporaryFolder folder = TemporaryFolder.builder().build(); + folder.create(); + + try { + File vaadinFolder = folder.newFolder("VAADIN"); + vaadinFolder.createNewFile(); + + // generate URL so it is not ending with / so that we test the + // correct method + String rootAbsolutePath = folder.getRoot().getAbsolutePath() + .replaceAll("\\\\", "/"); + if (rootAbsolutePath.endsWith("/")) { + rootAbsolutePath = rootAbsolutePath.substring(0, + rootAbsolutePath.length() - 1); + } + URL folderPath = new URL("file:///" + rootAbsolutePath); + + assertFalse("Folder on disk should not be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, folderPath)); + + // Test any path ending with / to be seen as a directory + assertFalse( + "Fake should not check the file system nor be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, + new URL("file:///fake/"))); + + File archiveFile = createJAR(folder); + Path tempArchive = archiveFile.toPath(); + String tempArchivePath = tempArchive.toString().replaceAll("\\\\", + "/"); + + assertFalse( + "Folder 'VAADIN' in jar should not be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, new URL( + "jar:file:///" + tempArchivePath + "!/VAADIN"))); + + assertFalse( + "File 'file.txt' inside jar should not be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, new URL( + "jar:file:///" + tempArchivePath + "!/file.txt"))); + + assertTrue( + "File 'file.txt' inside VAADIN folder within jar should be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, + new URL("jar:file:///" + tempArchivePath + + "!/VAADIN/file.txt"))); + + assertFalse( + "Directory 'folder' inside VAADIN folder within jar should not be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, + new URL("jar:file:///" + tempArchivePath + + "!/VAADIN/folder"))); + + assertFalse( + "File 'file.txt' outside of a jar should not be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, new URL( + "file:///" + rootAbsolutePath + "/file.txt"))); + + assertTrue( + "File 'file.txt' inside VAADIN folder outside of a jar should be an allowed resource.", + servlet.isAllowedVAADINResourceUrl(request, + new URL("file:///" + rootAbsolutePath + + "/VAADIN/file.txt"))); + + } finally { + folder.delete(); + } + } + + @Test + @SuppressWarnings("deprecation") + public void isAllowedVAADINResource_jarWarFileScheme_detectsAsStaticResources() + throws IOException, URISyntaxException, ServletException { + assertTrue("Can not run concurrently with other test", + VaadinServlet.openFileSystems.isEmpty()); + + VaadinServlet servlet = new VaadinServlet(); + servlet.init(new MockServletConfig()); + // this request isn't actually used for anything within the + // isAllowedVAADINResourceUrl calls, no need to configure it + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + + TemporaryFolder folder = TemporaryFolder.builder().build(); + folder.create(); + + try { + File archiveFile = createJAR(folder); + File warFile = createWAR(folder, archiveFile); + + // Instantiate URL stream handler factory to be able to handle war: + WarURLStreamHandlerFactory.getInstance(); + + URL folderResourceURL = new URL("jar:war:" + warFile.toURI().toURL() + + "!/" + archiveFile.getName() + "!/VAADIN/folder"); + + Assert.assertTrue( + "Should be evaluated as a static request because we cannot " + + "determine non-file resources within jar files.", + servlet.isAllowedVAADINResourceUrl(request, + folderResourceURL)); + + URL fileResourceURL = new URL("jar:war:" + warFile.toURI().toURL() + + "!/" + archiveFile.getName() + "!/VAADIN/file.txt"); + + Assert.assertTrue("Should be evaluated as a static request.", + servlet.isAllowedVAADINResourceUrl(request, + fileResourceURL)); + } finally { + folder.delete(); + } + } + + @Test + @SuppressWarnings("deprecation") + public void isAllowedVAADINResource_jarInAJar_detectsAsStaticResources() + throws IOException, URISyntaxException, ServletException { + assertTrue("Can not run concurrently with other test", + VaadinServlet.openFileSystems.isEmpty()); + + VaadinServlet servlet = new VaadinServlet(); + servlet.init(new MockServletConfig()); + // this request isn't actually used for anything within the + // isAllowedVAADINResourceUrl calls, no need to configure it + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + + TemporaryFolder folder = TemporaryFolder.builder().build(); + folder.create(); + + try { + File archiveFile = createJAR(folder); + File warFile = createWAR(folder, archiveFile); + + URL folderResourceURL = new URL("jar:" + warFile.toURI().toURL() + + "!/" + archiveFile.getName() + "!/VAADIN/folder"); + + Assert.assertTrue( + "Should be evaluated as a static request because we cannot " + + "determine non-file resources within jar files.", + servlet.isAllowedVAADINResourceUrl(request, + folderResourceURL)); + + URL fileResourceURL = new URL("jar:" + warFile.toURI().toURL() + + "!/" + archiveFile.getName() + "!/VAADIN/file.txt"); + + Assert.assertTrue("Should be evaluated as a static request.", + servlet.isAllowedVAADINResourceUrl(request, + fileResourceURL)); + + URL fileNonStaticResourceURL = new URL( + "jar:" + warFile.toURI().toURL() + "!/" + + archiveFile.getName() + "!/file.txt"); + + Assert.assertFalse( + "Should not be evaluated as a static request even within a " + + "jar because it's not within 'VAADIN' folder.", + servlet.isAllowedVAADINResourceUrl(request, + fileNonStaticResourceURL)); + } finally { + folder.delete(); + } + } + + @Test + public void openingJarFileSystemForDifferentFilesInSameJar_existingFileSystemIsUsed() + throws IOException, URISyntaxException, ServletException { + assertTrue("Can not run concurrently with other test", + VaadinServlet.openFileSystems.isEmpty()); + + VaadinServlet servlet = new VaadinServlet(); + servlet.init(new MockServletConfig()); + + TemporaryFolder folder = TemporaryFolder.builder().build(); + folder.create(); + + try { + File archiveFile = createJAR(folder); + String tempArchivePath = archiveFile.toPath().toString() + .replaceAll("\\\\", "/"); + + URL folderResourceURL = new URL( + "jar:file:///" + tempArchivePath + "!/VAADIN"); + + URL fileResourceURL = new URL( + "jar:file:///" + tempArchivePath + "!/file.txt"); + + servlet.getFileSystem(folderResourceURL.toURI()); + servlet.getFileSystem(fileResourceURL.toURI()); + + assertEquals("Same file should be marked for both resources", + (Integer) 2, VaadinServlet.openFileSystems.entrySet() + .iterator().next().getValue()); + servlet.closeFileSystem(folderResourceURL.toURI()); + assertEquals("Closing resource should be removed from jar uri", + (Integer) 1, VaadinServlet.openFileSystems.entrySet() + .iterator().next().getValue()); + servlet.closeFileSystem(fileResourceURL.toURI()); + assertTrue("Closing last resource should clear marking", + VaadinServlet.openFileSystems.isEmpty()); + + try { + FileSystems.getFileSystem(folderResourceURL.toURI()); + fail("Jar FileSystem should have been closed"); + } catch (FileSystemNotFoundException fsnfe) { + // This should happen as we should not have an open FileSystem + // here. + } + } finally { + folder.delete(); + } + } + + @Test + public void concurrentRequestsToJarResources_checksAreCorrect() + throws IOException, InterruptedException, ExecutionException, + URISyntaxException, ServletException { + assertTrue("Can not run concurrently with other test", + VaadinServlet.openFileSystems.isEmpty()); + + VaadinServlet servlet = new VaadinServlet(); + servlet.init(new MockServletConfig()); + // this request isn't actually used for anything within the + // isAllowedVAADINResourceUrl calls, no need to configure it + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + + TemporaryFolder folder = TemporaryFolder.builder().build(); + folder.create(); + + try { + File archiveFile = createJAR(folder); + String tempArchivePath = archiveFile.toPath().toString() + .replaceAll("\\\\", "/"); + + URL fileNotResourceURL = new URL( + "jar:file:///" + tempArchivePath + "!/file.txt"); + String fileNotResourceErrorMessage = "File file.text outside " + + "folder 'VAADIN' in jar should not be a static resource."; + + checkAllowedVAADINResourceConcurrently(servlet, request, + fileNotResourceURL, fileNotResourceErrorMessage, false); + ensureFileSystemsCleared(fileNotResourceURL); + + URL folderNotResourceURL = new URL( + "jar:file:///" + tempArchivePath + "!/VAADIN"); + String folderNotResourceErrorMessage = "Folder 'VAADIN' in " + + "jar should not be a static resource."; + + checkAllowedVAADINResourceConcurrently(servlet, request, + folderNotResourceURL, folderNotResourceErrorMessage, false); + ensureFileSystemsCleared(folderNotResourceURL); + + URL fileIsResourceURL = new URL( + "jar:file:///" + tempArchivePath + "!/VAADIN/file.txt"); + String fileIsResourceErrorMessage = "File 'file.txt' inside " + + "VAADIN folder within jar should be a static resource."; + + checkAllowedVAADINResourceConcurrently(servlet, request, + fileIsResourceURL, fileIsResourceErrorMessage, true); + ensureFileSystemsCleared(fileIsResourceURL); + } finally { + folder.delete(); + } + } + private HttpServletRequest createServletRequest(String servletPath, String pathInfo) { HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getServletPath()).thenReturn(servletPath); Mockito.when(request.getPathInfo()).thenReturn(pathInfo); - Mockito.when(request.getRequestURI()).thenReturn("/context"+pathInfo); + Mockito.when(request.getRequestURI()).thenReturn("/context" + pathInfo); Mockito.when(request.getContextPath()).thenReturn("/context"); return request; } + + /** + * Creates an archive file {@code fake.jar} that contains two + * {@code file.txt} files, one of which resides inside {@code VAADIN} + * directory. + * + * @param folder + * temporary folder that should house the archive file + * @return the archive file + * @throws IOException + */ + private File createJAR(TemporaryFolder folder) throws IOException { + File archiveFile = new File(folder.getRoot(), "fake.jar"); + archiveFile.createNewFile(); + Path tempArchive = archiveFile.toPath(); + + try (ZipOutputStream zipOutputStream = new ZipOutputStream( + Files.newOutputStream(tempArchive))) { + // Create a file to the zip + zipOutputStream.putNextEntry(new ZipEntry("/file.txt")); + zipOutputStream.closeEntry(); + // Create a directory to the zip + zipOutputStream.putNextEntry(new ZipEntry("VAADIN/")); + zipOutputStream.closeEntry(); + // Create a file to the directory + zipOutputStream.putNextEntry(new ZipEntry("VAADIN/file.txt")); + zipOutputStream.closeEntry(); + // Create another directory to the zip + zipOutputStream.putNextEntry(new ZipEntry("VAADIN/folder/")); + zipOutputStream.closeEntry(); + } + return archiveFile; + } + + private File createWAR(TemporaryFolder folder, File archiveFile) + throws IOException { + Path tempArchive = archiveFile.toPath(); + File warFile = new File(folder.getRoot(), "fake.war"); + warFile.createNewFile(); + Path warArchive = warFile.toPath(); + + try (ZipOutputStream warOutputStream = new ZipOutputStream( + Files.newOutputStream(warArchive))) { + // Create a file to the zip + warOutputStream.putNextEntry(new ZipEntry(archiveFile.getName())); + warOutputStream.write(Files.readAllBytes(tempArchive)); + + warOutputStream.closeEntry(); + } + return warFile; + } + + /** + * Performs the resource URL validity check in five threads simultaneously, + * and ensures that the results match the given expected value. + * + * @param servlet + * VaadinServlet instance + * @param request + * HttpServletRequest instance (does not need to be properly + * initialized) + * @param resourceURL + * the resource URL to validate + * @param resourceErrorMessage + * the error message if the validity check results don't match + * the expected value + * @param expected + * expected value from the validity check + * + * @throws InterruptedException + * @throws ExecutionException + */ + @SuppressWarnings("deprecation") + private void checkAllowedVAADINResourceConcurrently(VaadinServlet servlet, + HttpServletRequest request, URL resourceURL, + String resourceErrorMessage, boolean expected) + throws InterruptedException, ExecutionException { + int THREADS = 5; + + List<Callable<Result>> fileNotResource = IntStream.range(0, THREADS) + .mapToObj(i -> { + Callable<Result> callable = () -> { + try { + if (expected != servlet.isAllowedVAADINResourceUrl( + request, resourceURL)) { + throw new IllegalArgumentException( + resourceErrorMessage); + } + } catch (Exception e) { + return new Result(e); + } + return new Result(null); + }; + return callable; + }).collect(Collectors.toList()); + + ExecutorService executor = Executors.newFixedThreadPool(THREADS); + List<Future<Result>> futures = executor.invokeAll(fileNotResource); + List<String> exceptions = new ArrayList<>(); + + executor.shutdown(); + + for (Future<Result> resultFuture : futures) { + Result result = resultFuture.get(); + if (result.exception != null) { + exceptions.add(result.exception.getMessage()); + } + } + + assertTrue("There were exceptions in concurrent calls {" + exceptions + + "}", exceptions.isEmpty()); + } + + private void ensureFileSystemsCleared(URL fileResourceURL) + throws URISyntaxException { + assertFalse("URI should have been cleared", + VaadinServlet.openFileSystems + .containsKey(fileResourceURL.toURI())); + try { + FileSystems.getFileSystem(fileResourceURL.toURI()); + fail("FileSystem for file resource should be closed"); + } catch (FileSystemNotFoundException fsnfe) { + // This should happen as we should not have an open FileSystem + // here. + } + } + + private static class Result { + final Exception exception; + + Result(Exception exception) { + this.exception = exception; + } + } } diff --git a/server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java b/server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java new file mode 100644 index 0000000000..5197c1456e --- /dev/null +++ b/server/src/test/java/com/vaadin/server/WarURLStreamHandlerFactory.java @@ -0,0 +1,196 @@ +package com.vaadin.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.security.Permission; + +/** + * Test factory for URL stream protocol handlers, needed for WAR handling in + * {@link VaadinServletTest}. Cherry-picked from Flow, some of the + * implementation details are not needed for Vaadin 8 at the moment, but they + * are left in because they aren't interfering either. + */ +public class WarURLStreamHandlerFactory + implements URLStreamHandlerFactory, Serializable { + + private static final String WAR_PROTOCOL = "war"; + + // Singleton instance + private static volatile WarURLStreamHandlerFactory instance = null; + + private final boolean registered; + + /** + * Obtain a reference to the singleton instance. It is recommended that + * callers check the value of {@link #isRegistered()} before using the + * returned instance. + * + * @return A reference to the singleton instance + */ + public static WarURLStreamHandlerFactory getInstance() { + getInstanceInternal(true); + return instance; + } + + private static WarURLStreamHandlerFactory getInstanceInternal( + boolean register) { + // Double checked locking. OK because instance is volatile. + if (instance == null) { + synchronized (WarURLStreamHandlerFactory.class) { + if (instance == null) { + instance = new WarURLStreamHandlerFactory(register); + } + } + } + return instance; + } + + private WarURLStreamHandlerFactory(boolean register) { + // Hide default constructor + // Singleton pattern to ensure there is only one instance of this + // factory + registered = register; + if (register) { + URL.setURLStreamHandlerFactory(this); + } + } + + public boolean isRegistered() { + return registered; + } + + /** + * Register this factory with the JVM. May be called more than once. The + * implementation ensures that registration only occurs once. + * + * @return <code>true</code> if the factory is already registered with the + * JVM or was successfully registered as a result of this call. + * <code>false</code> if the factory was disabled prior to this + * call. + */ + public static boolean register() { + return getInstanceInternal(true).isRegistered(); + } + + /** + * Prevent this this factory from registering with the JVM. May be called + * more than once. + * + * @return <code>true</code> if the factory is already disabled or was + * successfully disabled as a result of this call. + * <code>false</code> if the factory was already registered prior to + * this call. + */ + public static boolean disable() { + return !getInstanceInternal(false).isRegistered(); + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + + // Tomcat's handler always takes priority so applications can't override + // it. + if (WAR_PROTOCOL.equals(protocol)) { + return new WarHandler(); + } + + // Unknown protocol + return null; + } + + public static class WarHandler extends URLStreamHandler + implements Serializable { + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return new WarURLConnection(u); + } + + @Override + protected void setURL(URL u, String protocol, String host, int port, + String authority, String userInfo, String path, String query, + String ref) { + if (path.startsWith("file:") && !path.startsWith("file:/")) { + /* + * Work around a problem with the URLs in the security policy + * file. On Windows, the use of ${catalina.[home|base]} in the + * policy file results in codebase URLs of the form file:C:/... + * when they should be file:/C:/... + * + * For file: and jar: URLs, the JRE compensates for this. It + * does not compensate for this for war:file:... URLs. + * Therefore, we do that here + */ + path = "file:/" + path.substring(5); + } + super.setURL(u, protocol, host, port, authority, userInfo, path, + query, ref); + } + + } + + public static class WarURLConnection extends URLConnection + implements Serializable { + + private final URLConnection wrappedJarUrlConnection; + private boolean connected; + + protected WarURLConnection(URL url) throws IOException { + super(url); + URL innerJarUrl = warToJar(url); + wrappedJarUrlConnection = innerJarUrl.openConnection(); + } + + @Override + public void connect() throws IOException { + if (!connected) { + wrappedJarUrlConnection.connect(); + connected = true; + } + } + + @Override + public InputStream getInputStream() throws IOException { + connect(); + return wrappedJarUrlConnection.getInputStream(); + } + + @Override + public Permission getPermission() throws IOException { + return wrappedJarUrlConnection.getPermission(); + } + + @Override + public long getLastModified() { + return wrappedJarUrlConnection.getLastModified(); + } + + @Override + public int getContentLength() { + return wrappedJarUrlConnection.getContentLength(); + } + + @Override + public long getContentLengthLong() { + return wrappedJarUrlConnection.getContentLengthLong(); + } + + public static URL warToJar(URL warUrl) throws MalformedURLException { + // Assumes that the spec is absolute and starts war:file:/... + String file = warUrl.getFile(); + if (file.contains("*/")) { + file = file.replaceFirst("\\*/", "!/"); + } else if (file.contains("^/")) { + file = file.replaceFirst("\\^/", "!/"); + } + + return new URL("jar", warUrl.getHost(), warUrl.getPort(), file); + } + } +}
\ No newline at end of file |