diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java index bc6866ba0b52..730021871333 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java @@ -52,6 +52,7 @@ enum Ambiguous { SEGMENT, SEPARATOR, + ENCODING, PARAM } @@ -159,6 +160,11 @@ static Immutable from(String scheme, String host, int port, String pathQuery) */ boolean hasAmbiguousParameter(); + /** + * @return True if the URI has an encoded '%' character. + */ + boolean hasAmbiguousEncoding(); + default URI toURI() { try @@ -386,6 +392,12 @@ public boolean hasAmbiguousParameter() return _ambiguous.contains(Ambiguous.PARAM); } + @Override + public boolean hasAmbiguousEncoding() + { + return _ambiguous.contains(Ambiguous.ENCODING); + } + @Override public String toString() { @@ -727,6 +739,12 @@ public boolean hasAmbiguousParameter() return _ambiguous.contains(Ambiguous.PARAM); } + @Override + public boolean hasAmbiguousEncoding() + { + return _ambiguous.contains(Ambiguous.ENCODING); + } + public Mutable normalize() { HttpScheme scheme = _scheme == null ? null : HttpScheme.CACHE.get(_scheme); @@ -884,7 +902,7 @@ private void parse(State state, final String uri) int segment = 0; // the start of the current segment within the path boolean encoded = false; // set to true if the path contains % encoded characters boolean dot = false; // set to true if the path containers . or .. segments - int escapedSlash = 0; // state of parsing a %2f + int escapedTwo = 0; // state of parsing a %2 int end = uri.length(); for (int i = 0; i < end; i++) { @@ -920,7 +938,7 @@ private void parse(State state, final String uri) break; case '%': encoded = true; - escapedSlash = 1; + escapedTwo = 1; mark = pathMark = segment = i; state = State.PATH; break; @@ -972,7 +990,7 @@ private void parse(State state, final String uri) case '%': // must have be in an encoded path encoded = true; - escapedSlash = 1; + escapedTwo = 1; state = State.PATH; break; case '#': @@ -1120,19 +1138,24 @@ else if (c == '/') break; case '%': encoded = true; - escapedSlash = 1; + escapedTwo = 1; break; case '2': - escapedSlash = escapedSlash == 1 ? 2 : 0; + escapedTwo = escapedTwo == 1 ? 2 : 0; break; case 'f': case 'F': - if (escapedSlash == 2) + if (escapedTwo == 2) _ambiguous.add(Ambiguous.SEPARATOR); - escapedSlash = 0; + escapedTwo = 0; + break; + case '5': + if (escapedTwo == 2) + _ambiguous.add(Ambiguous.ENCODING); + escapedTwo = 0; break; default: - escapedSlash = 0; + escapedTwo = 0; break; } break; @@ -1266,4 +1289,4 @@ else if (param && ambiguous == Boolean.FALSE) } } } -} \ No newline at end of file +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java index 0eb0d557ddd1..cddb35dc3af3 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java @@ -58,6 +58,10 @@ public enum Violation implements ComplianceViolation * Allow ambiguous path parameters within a URI segment e.g. /foo/..;/bar */ AMBIGUOUS_PATH_PARAMETER("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path parameter"), + /** + * Allow ambiguous path encoding within a URI segment e.g. /%2557EB-INF + */ + AMBIGUOUS_PATH_ENCODING("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path encoding"), /** * Allow Non canonical ambiguous paths. eg /foo/x%2f%2e%2e%/bar provided to applications as /foo/x/../bar */ @@ -94,15 +98,15 @@ public String getDescription() /** * The default compliance mode that extends RFC3986 compliance with additional violations to avoid most ambiguous URIs. * This mode does allow {@link Violation#AMBIGUOUS_PATH_SEPARATOR}, but disallows - * {@link Violation#AMBIGUOUS_PATH_PARAMETER} and {@link Violation#AMBIGUOUS_PATH_SEGMENT}. + * {@link Violation#AMBIGUOUS_PATH_PARAMETER}, {@link Violation#AMBIGUOUS_PATH_SEGMENT} and {@link Violation#AMBIGUOUS_PATH_ENCODING}. * Ambiguous paths are not allowed by {@link Violation#NON_CANONICAL_AMBIGUOUS_PATHS}. */ public static final UriCompliance DEFAULT = new UriCompliance("DEFAULT", of(Violation.AMBIGUOUS_PATH_SEPARATOR)); /** - * LEGACY compliance mode that models Jetty-9.4 behavior by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT} and {@link Violation#AMBIGUOUS_PATH_SEPARATOR} + * LEGACY compliance mode that models Jetty-9.4 behavior by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT}, {@link Violation#AMBIGUOUS_PATH_SEPARATOR} and {@link Violation#AMBIGUOUS_PATH_ENCODING}. */ - public static final UriCompliance LEGACY = new UriCompliance("LEGACY", of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.AMBIGUOUS_PATH_SEPARATOR)); + public static final UriCompliance LEGACY = new UriCompliance("LEGACY", of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_ENCODING)); /** * Compliance mode that exactly follows RFC3986, including allowing all additional ambiguous URI Violations, diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 95866fb99104..997dcf2acb6c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -1702,6 +1702,8 @@ public void setMetaData(MetaData.Request request) throw new BadMessageException("Ambiguous segment in URI"); if (uri.hasAmbiguousParameter() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER))) throw new BadMessageException("Ambiguous path parameter in URI"); + if (uri.hasAmbiguousEncoding() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING))) + throw new BadMessageException("Ambiguous path encoding in URI"); } if (uri.isAbsolute() && uri.hasAuthority() && uri.getPath() != null) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java index bfb3cf1a8570..5172c42386bb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java @@ -428,7 +428,7 @@ protected void sendWelcome(HttpContent content, String pathInContext, boolean en return; } - RequestDispatcher dispatcher = context.getRequestDispatcher(welcome); + RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome)); if (dispatcher != null) { // Forward to the index diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 2661e609e235..c6aa51937a88 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -1773,6 +1773,23 @@ public void testAmbiguousPaths() throws Exception startsWith("HTTP/1.1 200"), containsString("pathInfo=/path/ambiguous/.././info"))); } + + @Test + public void testAmbiguousEncoding() throws Exception + { + _handler._checker = (request, response) -> true; + String request = "GET /ambiguous/encoded/%25/path HTTP/1.0\r\n" + + "Host: whatever\r\n" + + "\r\n"; + _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT); + assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400")); + _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY); + assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200")); + _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986); + assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200")); + _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE); + assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200")); + } @Test public void testPushBuilder() diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java index 6dd46804f035..a0fcfa038d46 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java @@ -56,6 +56,7 @@ * appropriate. This means that when not in development mode, the servlet must be * restarted before changed content will be served.

*/ +@Deprecated public class ConcatServlet extends HttpServlet { private boolean _development; @@ -120,7 +121,8 @@ else if (!type.equals(t)) } } - RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path); + // Use the original string and not the decoded path as the Dispatcher will decode again. + RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(part); if (dispatcher != null) dispatchers.add(dispatcher); } diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java index 687354e3ce14..d5abac44bdae 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/WelcomeFilter.java @@ -22,6 +22,8 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; +import org.eclipse.jetty.util.URIUtil; + /** * Welcome Filter * This filter can be used to server an index file for a directory @@ -36,6 +38,7 @@ * * Requests to "/some/directory" will be redirected to "/some/directory/". */ +@Deprecated public class WelcomeFilter implements Filter { private String welcome; @@ -56,7 +59,10 @@ public void doFilter(ServletRequest request, { String path = ((HttpServletRequest)request).getServletPath(); if (welcome != null && path.endsWith("/")) - request.getRequestDispatcher(path + welcome).forward(request, response); + { + String uriInContext = URIUtil.encodePath(URIUtil.addPaths(path, welcome)); + request.getRequestDispatcher(uriInContext).forward(request, response); + } else chain.doFilter(request, response); } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java index fad32e1cafcf..79be0fca894c 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java @@ -21,12 +21,15 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.Stream; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -36,7 +39,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -48,10 +56,11 @@ public class ConcatServletTest private LocalConnector connector; @BeforeEach - public void prepareServer() throws Exception + public void prepareServer() { server = new Server(); connector = new LocalConnector(server); + connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986); server.addConnector(connector); } @@ -73,7 +82,7 @@ public void testConcatenation() throws Exception ServletHolder resourceServletHolder = new ServletHolder(new HttpServlet() { @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String includedURI = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); response.getOutputStream().println(includedURI); @@ -107,7 +116,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } @Test - public void testWEBINFResourceIsNotServed() throws Exception + public void testDirectoryNotAccessible() throws Exception { File directoryFile = MavenTestingUtils.getTargetTestingDir(); Path directoryPath = directoryFile.toPath(); @@ -129,9 +138,8 @@ public void testWEBINFResourceIsNotServed() throws Exception // Verify that I can get the file programmatically, as required by the spec. assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); - // Having a path segment and then ".." triggers a special case - // that the ConcatServlet must detect and avoid. - String uri = contextPath + concatPath + "?/trick/../WEB-INF/one.js"; + // Make sure ConcatServlet cannot see file system files. + String uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); String request = "GET " + uri + " HTTP/1.1\r\n" + "Host: localhost\r\n" + @@ -139,35 +147,59 @@ public void testWEBINFResourceIsNotServed() throws Exception "\r\n"; String response = connector.getResponse(request); assertTrue(response.startsWith("HTTP/1.1 404 ")); + } - // Make sure ConcatServlet behaves well if it's case insensitive. - uri = contextPath + concatPath + "?/trick/../web-inf/one.js"; - request = - "GET " + uri + " HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Connection: close\r\n" + - "\r\n"; - response = connector.getResponse(request); - assertTrue(response.startsWith("HTTP/1.1 404 ")); + public static Stream webInfTestExamples() + { + return Stream.of( + // Cannot access WEB-INF. + Arguments.of("?/WEB-INF/", "HTTP/1.1 404 "), + Arguments.of("?/WEB-INF/one.js", "HTTP/1.1 404 "), + + // Having a path segment and then ".." triggers a special case that the ConcatServlet must detect and avoid. + Arguments.of("?/trick/../WEB-INF/one.js", "HTTP/1.1 404 "), + + // Make sure ConcatServlet behaves well if it's case insensitive. + Arguments.of("?/trick/../web-inf/one.js", "HTTP/1.1 404 "), + + // Make sure ConcatServlet behaves well if encoded. + Arguments.of("?/trick/..%2FWEB-INF%2Fone.js", "HTTP/1.1 404 "), + Arguments.of("?/%2557EB-INF/one.js", "HTTP/1.1 500 "), + Arguments.of("?/js/%252e%252e/WEB-INF/one.js", "HTTP/1.1 500 ") + ); + } - // Make sure ConcatServlet behaves well if encoded. - uri = contextPath + concatPath + "?/trick/..%2FWEB-INF%2Fone.js"; - request = - "GET " + uri + " HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Connection: close\r\n" + - "\r\n"; - response = connector.getResponse(request); - assertTrue(response.startsWith("HTTP/1.1 404 ")); + @ParameterizedTest + @MethodSource("webInfTestExamples") + public void testWEBINFResourceIsNotServed(String querystring, String expectedStatus) throws Exception + { + File directoryFile = MavenTestingUtils.getTargetTestingDir(); + Path directoryPath = directoryFile.toPath(); + Path hiddenDirectory = directoryPath.resolve("WEB-INF"); + Files.createDirectories(hiddenDirectory); + Path hiddenResource = hiddenDirectory.resolve("one.js"); + try (OutputStream output = Files.newOutputStream(hiddenResource)) + { + output.write("function() {}".getBytes(StandardCharsets.UTF_8)); + } - // Make sure ConcatServlet cannot see file system files. - uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); - request = + String contextPath = ""; + WebAppContext context = new WebAppContext(server, directoryPath.toString(), contextPath); + server.setHandler(context); + String concatPath = "/concat"; + context.addServlet(ConcatServlet.class, concatPath); + server.start(); + + // Verify that I can get the file programmatically, as required by the spec. + assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); + + String uri = contextPath + concatPath + querystring; + String request = "GET " + uri + " HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n"; - response = connector.getResponse(request); - assertTrue(response.startsWith("HTTP/1.1 404 ")); + String response = connector.getResponse(request); + assertThat(response, startsWith(expectedStatus)); } } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/WelcomeFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/WelcomeFilterTest.java new file mode 100644 index 000000000000..2e3cbae99526 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/WelcomeFilterTest.java @@ -0,0 +1,141 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.stream.Stream; +import javax.servlet.DispatcherType; + +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.webapp.WebAppContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class WelcomeFilterTest +{ + private Server server; + private LocalConnector connector; + + @BeforeEach + public void prepareServer() throws Exception + { + server = new Server(); + connector = new LocalConnector(server); + connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986); + server.addConnector(connector); + + Path directoryPath = MavenTestingUtils.getTargetTestingDir().toPath(); + Files.createDirectories(directoryPath); + Path welcomeResource = directoryPath.resolve("welcome.html"); + try (OutputStream output = Files.newOutputStream(welcomeResource)) + { + output.write("

welcome page

".getBytes(StandardCharsets.UTF_8)); + } + + Path otherResource = directoryPath.resolve("other.html"); + try (OutputStream output = Files.newOutputStream(otherResource)) + { + output.write("

other resource

".getBytes(StandardCharsets.UTF_8)); + } + + Path hiddenDirectory = directoryPath.resolve("WEB-INF"); + Files.createDirectories(hiddenDirectory); + Path hiddenResource = hiddenDirectory.resolve("one.js"); + try (OutputStream output = Files.newOutputStream(hiddenResource)) + { + output.write("CONFIDENTIAL".getBytes(StandardCharsets.UTF_8)); + } + + Path hiddenWelcome = hiddenDirectory.resolve("index.html"); + try (OutputStream output = Files.newOutputStream(hiddenWelcome)) + { + output.write("CONFIDENTIAL".getBytes(StandardCharsets.UTF_8)); + } + + WebAppContext context = new WebAppContext(server, directoryPath.toString(), "/"); + server.setHandler(context); + String concatPath = "/*"; + + FilterHolder filterHolder = new FilterHolder(new WelcomeFilter()); + filterHolder.setInitParameter("welcome", "welcome.html"); + context.addFilter(filterHolder, concatPath, EnumSet.of(DispatcherType.REQUEST)); + server.start(); + + // Verify that I can get the file programmatically, as required by the spec. + assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); + } + + @AfterEach + public void destroy() throws Exception + { + if (server != null) + server.stop(); + } + + public static Stream argumentsStream() + { + return Stream.of( + // Normal requests for the directory are redirected to the welcome page. + Arguments.of("/", new String[]{"HTTP/1.1 200 ", "

welcome page

"}), + + // Try a normal resource (will bypass the filter). + Arguments.of("/other.html", new String[]{"HTTP/1.1 200 ", "

other resource

"}), + + // Cannot access files in WEB-INF. + Arguments.of("/WEB-INF/one.js", new String[]{"HTTP/1.1 404 "}), + + // Cannot serve welcome from WEB-INF. + Arguments.of("/WEB-INF/", new String[]{"HTTP/1.1 404 "}), + + // Try to trick the filter into serving a protected resource. + Arguments.of("/WEB-INF/one.js#/", new String[]{"HTTP/1.1 404 "}), + Arguments.of("/js/../WEB-INF/one.js#/", new String[]{"HTTP/1.1 404 "}), + + // Test the URI is not double decoded in the dispatcher. + Arguments.of("/%2557EB-INF/one.js%23/", new String[]{"HTTP/1.1 404 "}) + ); + } + + @ParameterizedTest + @MethodSource("argumentsStream") + public void testWelcomeFilter(String uri, String[] contains) throws Exception + { + String request = + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + String response = connector.getResponse(request); + for (String s : contains) + { + assertThat(response, containsString(s)); + } + } +} diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppDefaultServletTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppDefaultServletTest.java new file mode 100644 index 000000000000..2ab5aeb110b9 --- /dev/null +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppDefaultServletTest.java @@ -0,0 +1,140 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.webapp; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.IO; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class WebAppDefaultServletTest +{ + private Server server; + private LocalConnector connector; + + @BeforeEach + public void prepareServer() throws Exception + { + server = new Server(); + connector = new LocalConnector(server); + connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986); + server.addConnector(connector); + + Path directoryPath = MavenTestingUtils.getTargetTestingDir().toPath(); + IO.delete(directoryPath.toFile()); + Files.createDirectories(directoryPath); + Path welcomeResource = directoryPath.resolve("index.html"); + try (OutputStream output = Files.newOutputStream(welcomeResource)) + { + output.write("

welcome page

".getBytes(StandardCharsets.UTF_8)); + } + + Path otherResource = directoryPath.resolve("other.html"); + try (OutputStream output = Files.newOutputStream(otherResource)) + { + output.write("

other resource

".getBytes(StandardCharsets.UTF_8)); + } + + Path hiddenDirectory = directoryPath.resolve("WEB-INF"); + Files.createDirectories(hiddenDirectory); + Path hiddenResource = hiddenDirectory.resolve("one.js"); + try (OutputStream output = Files.newOutputStream(hiddenResource)) + { + output.write("this is confidential".getBytes(StandardCharsets.UTF_8)); + } + + // Create directory to trick resource service. + Path hackPath = directoryPath.resolve("%57EB-INF/one.js#/"); + Files.createDirectories(hackPath); + try (OutputStream output = Files.newOutputStream(hackPath.resolve("index.html"))) + { + output.write("this content does not matter".getBytes(StandardCharsets.UTF_8)); + } + + Path standardHashDir = directoryPath.resolve("welcome#"); + Files.createDirectories(standardHashDir); + try (OutputStream output = Files.newOutputStream(standardHashDir.resolve("index.html"))) + { + output.write("standard hash dir welcome".getBytes(StandardCharsets.UTF_8)); + } + + WebAppContext context = new WebAppContext(server, directoryPath.toString(), "/"); + server.setHandler(context); + server.start(); + + // Verify that I can get the file programmatically, as required by the spec. + assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); + } + + @AfterEach + public void destroy() throws Exception + { + if (server != null) + server.stop(); + } + + public static Stream argumentsStream() + { + return Stream.of( + Arguments.of("/WEB-INF/", new String[]{"HTTP/1.1 404 "}), + Arguments.of("/welcome%23/", new String[]{"HTTP/1.1 200 ", "standard hash dir welcome"}), + + // Normal requests for the directory are redirected to the welcome page. + Arguments.of("/", new String[]{"HTTP/1.1 200 ", "

welcome page

"}), + + // We can be served other resources. + Arguments.of("/other.html", new String[]{"HTTP/1.1 200 ", "

other resource

"}), + + // The ContextHandler will filter these ones out as as WEB-INF is a protected target. + Arguments.of("/WEB-INF/one.js#/", new String[]{"HTTP/1.1 404 "}), + Arguments.of("/js/../WEB-INF/one.js#/", new String[]{"HTTP/1.1 404 "}), + + // Test the URI is not double decoded by the dispatcher that serves the welcome file (we get index.html not one.js). + Arguments.of("/%2557EB-INF/one.js%23/", new String[]{"HTTP/1.1 200 ", "this content does not matter"}) + ); + } + + @ParameterizedTest + @MethodSource("argumentsStream") + public void testResourceService(String uri, String[] contains) throws Exception + { + String request = + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + String response = connector.getResponse(request); + for (String s : contains) + { + assertThat(response, containsString(s)); + } + } +}