Fixes #6263 - Review URI encoding in ConcatServlet & WelcomeFilter.
Review URI encoding in ConcatServlet & WelcomeFilter and improve testing. Signed-off-by: Lachlan Roberts <lachlan@webtide.com> Signed-off-by: Simone Bordet <simone.bordet@gmail.com> Co-authored-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
9cb93432b3
commit
1c05b0bcb1
|
@ -240,7 +240,7 @@ public class ResourceService
|
|||
// Find the content
|
||||
content = _contentFactory.getContent(pathInContext, response.getBufferSize());
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.info("content={}", content);
|
||||
LOG.debug("content={}", content);
|
||||
|
||||
// Not found?
|
||||
if (content == null || !content.getResource().exists())
|
||||
|
@ -430,7 +430,7 @@ public class ResourceService
|
|||
return;
|
||||
}
|
||||
|
||||
RequestDispatcher dispatcher = context.getRequestDispatcher(welcome);
|
||||
RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome));
|
||||
if (dispatcher != null)
|
||||
{
|
||||
// Forward to the index
|
||||
|
|
|
@ -61,6 +61,7 @@ import org.eclipse.jetty.util.URIUtil;
|
|||
* appropriate. This means that when not in development mode, the servlet must be
|
||||
* restarted before changed content will be served.</p>
|
||||
*/
|
||||
@Deprecated
|
||||
public class ConcatServlet extends HttpServlet
|
||||
{
|
||||
private boolean _development;
|
||||
|
@ -125,7 +126,8 @@ public class ConcatServlet extends HttpServlet
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import javax.servlet.ServletRequest;
|
|||
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
|
||||
|
@ -41,6 +43,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||
*
|
||||
* Requests to "/some/directory" will be redirected to "/some/directory/".
|
||||
*/
|
||||
@Deprecated
|
||||
public class WelcomeFilter implements Filter
|
||||
{
|
||||
private String welcome;
|
||||
|
@ -61,7 +64,10 @@ public class WelcomeFilter implements Filter
|
|||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.io.StringReader;
|
|||
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;
|
||||
|
@ -41,7 +42,12 @@ import org.eclipse.jetty.webapp.WebAppContext;
|
|||
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;
|
||||
|
@ -112,7 +118,7 @@ public class ConcatServletTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testWEBINFResourceIsNotServed() throws Exception
|
||||
public void testDirectoryNotAccessible() throws Exception
|
||||
{
|
||||
File directoryFile = MavenTestingUtils.getTargetTestingDir();
|
||||
Path directoryPath = directoryFile.toPath();
|
||||
|
@ -134,9 +140,8 @@ public class ConcatServletTest
|
|||
// 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" +
|
||||
|
@ -144,35 +149,59 @@ public class ConcatServletTest
|
|||
"\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 =
|
||||
public static Stream<Arguments> 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 ")
|
||||
);
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
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 "));
|
||||
|
||||
// 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 "));
|
||||
|
||||
// Make sure ConcatServlet cannot see file system files.
|
||||
uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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.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);
|
||||
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("<h1>welcome page</h1>".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
Path otherResource = directoryPath.resolve("other.html");
|
||||
try (OutputStream output = Files.newOutputStream(otherResource))
|
||||
{
|
||||
output.write("<h1>other resource</h1>".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<Arguments> argumentsStream()
|
||||
{
|
||||
return Stream.of(
|
||||
// Normal requests for the directory are redirected to the welcome page.
|
||||
Arguments.of("/", new String[]{"HTTP/1.1 200 ", "<h1>welcome page</h1>"}),
|
||||
|
||||
// Try a normal resource (will bypass the filter).
|
||||
Arguments.of("/other.html", new String[]{"HTTP/1.1 200 ", "<h1>other resource</h1>"}),
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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.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);
|
||||
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("<h1>welcome page</h1>".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
Path otherResource = directoryPath.resolve("other.html");
|
||||
try (OutputStream output = Files.newOutputStream(otherResource))
|
||||
{
|
||||
output.write("<h1>other resource</h1>".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<Arguments> 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 ", "<h1>welcome page</h1>"}),
|
||||
|
||||
// We can be served other resources.
|
||||
Arguments.of("/other.html", new String[]{"HTTP/1.1 200 ", "<h1>other resource</h1>"}),
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue