From 2e75febef1c7c0d9a49e87f4c3b191afb585862c Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Tue, 29 Jun 2021 00:07:56 +0200
Subject: [PATCH] Issue #6473 - Improve alias checking in PathResource.
* Reverted %-escape handling for URI query parts.
* Performing canonicalization in ServletContext.getResource(),
and improving alias checking in ContextHandler.getResource().
* Performing canonicalization checks in Resource.addPath() to avoid
navigation above of the root.
* Test added and fixed.
* Various cleanups.
* Improved javadoc and comments
Signed-off-by: Simone Bordet
---
.../java/org/eclipse/jetty/http/HttpURI.java | 67 +++++++++----------
.../org/eclipse/jetty/http/HttpURITest.java | 20 ++++++
.../maven/plugin/JettyWebAppContext.java | 16 +++--
.../jetty/rewrite/handler/RedirectUtil.java | 4 +-
.../rewrite/handler/ValidUrlRuleTest.java | 14 +++-
.../jetty/server/handler/ContextHandler.java | 12 ++--
.../jetty/server/handler/ResourceHandler.java | 2 +
.../jetty/server/HttpConnectionTest.java | 6 ++
.../ContextHandlerGetResourceTest.java | 21 ++++--
.../eclipse/jetty/servlet/RequestURITest.java | 45 ++++++++++++-
.../java/org/eclipse/jetty/util/URIUtil.java | 1 -
.../jetty/util/resource/FileResource.java | 7 +-
.../jetty/util/resource/PathResource.java | 25 ++++---
.../eclipse/jetty/util/resource/Resource.java | 8 ++-
.../jetty/util/resource/URLResource.java | 7 +-
.../jetty/util/URIUtilCanonicalPathTest.java | 20 ++++++
.../jetty/util/resource/ResourceTest.java | 18 +++++
17 files changed, 216 insertions(+), 77 deletions(-)
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 5dc6db1302aa..0a2e758caf39 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
@@ -36,7 +36,7 @@
/**
* Http URI.
* Parse an HTTP URI from a string or byte array. Given a URI
- * http://user@host:port/path;param1/%2e/info;param2?query#fragment
+ * {@code http://user@host:port/path;param1/%2e/info;param2?query#fragment}
* this class will split it into the following optional elements:
* - {@link #getScheme()} - http:
* - {@link #getAuthority()} - //name@host:port
@@ -65,11 +65,13 @@
* Thus this class avoid and/or detects such ambiguities. Furthermore, by decoding characters and
* removing parameters before relative path normalization, ambiguous paths will be resolved in such
* a way to be non-standard-but-non-ambiguous to down stream interpretation of the decoded path string.
- * The violations are recorded and available by API such as {@link #hasViolation(Violation)} so that requests
+ * The violations are recorded and available by API such as {@link #hasAmbiguousSegment()} so that requests
* containing them may be rejected in case the non-standard-but-non-ambiguous interpretations
- * are not satisfactory for a given compliance configuration. Implementations that wish to
- * process ambiguous URI paths must configure the compliance modes to accept them and then perform
- * their own decoding of {@link #getPath()}.
+ * are not satisfactory for a given compliance configuration.
+ *
+ *
+ * Implementations that wish to process ambiguous URI paths must configure the compliance modes
+ * to accept them and then perform their own decoding of {@link #getPath()}.
*
*
* If there are multiple path parameters, only the last one is returned by {@link #getParam()}.
@@ -95,30 +97,30 @@ private enum State
/**
* Violations of safe URI interpretations
*/
- public enum Violation
+ enum Violation
{
/**
- * Ambiguous path segments e.g. /foo/%2E%2E/bar
+ * Ambiguous path segments e.g. {@code /foo/%2E%2E/bar}
*/
SEGMENT("Ambiguous path segments"),
/**
- * Ambiguous path separator within a URI segment e.g. /foo%2Fbar
+ * Ambiguous path separator within a URI segment e.g. {@code /foo%2Fbar}
*/
SEPARATOR("Ambiguous path separator"),
/**
- * Ambiguous path parameters within a URI segment e.g. /foo/..;/bar
+ * Ambiguous path parameters within a URI segment e.g. {@code /foo/..;/bar}
*/
PARAM("Ambiguous path parameters"),
/**
- * Ambiguous double encoding within a URI segment e.g. /%2557EB-INF
+ * Ambiguous double encoding within a URI segment e.g. {@code /%2557EB-INF}
*/
ENCODING("Ambiguous double encoding"),
/**
- * Ambiguous empty segments e.g. /foo//bar
+ * Ambiguous empty segments e.g. {@code /foo//bar}
*/
EMPTY("Ambiguous empty segments"),
/**
- * Non standard UTF-16 encoding eg /foo%u2192bar
.
+ * Non standard UTF-16 encoding eg {@code /foo%u2192bar}.
*/
UTF16("Non standard UTF-16 encoding");
@@ -338,7 +340,7 @@ private void parse(State state, final String uri, final int offset, final int en
int encodedValue = 0; // the partial encoded value
boolean dot = false; // set to true if the path contains . or .. segments
- for (int i = 0; i < end; i++)
+ for (int i = offset; i < end; i++)
{
char c = uri.charAt(i);
@@ -567,6 +569,8 @@ else if (c == '/')
switch (encodedValue)
{
case 0:
+ // Byte 0 cannot be present in a UTF-8 sequence in any position
+ // other than as the NUL ASCII byte which we do not wish to allow.
throw new IllegalArgumentException("Illegal character in path");
case '/':
_violations.add(Violation.SEPARATOR);
@@ -653,26 +657,11 @@ else if (c == '/')
}
case QUERY:
{
- switch (c)
+ if (c == '#')
{
- case '%':
- encodedCharacters = 2;
- break;
- case 'u':
- case 'U':
- if (encodedCharacters == 1)
- _violations.add(Violation.UTF16);
- encodedCharacters = 0;
- break;
- case '#':
- _query = uri.substring(mark, i);
- mark = i + 1;
- state = State.FRAGMENT;
- encodedCharacters = 0;
- break;
- default:
- encodedCharacters = 0;
- break;
+ _query = uri.substring(mark, i);
+ mark = i + 1;
+ state = State.FRAGMENT;
}
break;
}
@@ -687,7 +676,9 @@ else if (c == '/')
break;
}
default:
+ {
throw new IllegalStateException(state.toString());
+ }
}
}
@@ -741,8 +732,8 @@ else if (_path != null)
{
// The RFC requires this to be canonical before decoding, but this can leave dot segments and dot dot segments
// which are not canonicalized and could be used in an attempt to bypass security checks.
- String decodeNonCanonical = URIUtil.decodePath(_path);
- _decodedPath = URIUtil.canonicalPath(decodeNonCanonical);
+ String decodedNonCanonical = URIUtil.decodePath(_path);
+ _decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
if (_decodedPath == null)
throw new IllegalArgumentException("Bad URI");
}
@@ -763,7 +754,8 @@ private void checkSegment(String uri, int segment, int end, boolean param)
// This method is called once for every segment parsed.
// A URI like "/foo/" has two segments: "foo" and an empty segment.
// Empty segments are only ambiguous if they are not the last segment
- // So if this method is called for any segment and we have previously seen an empty segment, then it was ambiguous
+ // So if this method is called for any segment and we have previously
+ // seen an empty segment, then it was ambiguous.
if (_emptySegment)
_violations.add(Violation.EMPTY);
@@ -843,7 +835,8 @@ public boolean hasAmbiguousEncoding()
}
/**
- * @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousSeparator()}.
+ * @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousEmptySegment()}
+ * or {@link #hasAmbiguousSeparator()} or {@link #hasAmbiguousParameter()}
*/
public boolean isAmbiguous()
{
@@ -858,7 +851,7 @@ public boolean hasViolations()
return !_violations.isEmpty();
}
- public boolean hasViolation(Violation violation)
+ boolean hasViolation(Violation violation)
{
return _violations.contains(violation);
}
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
index 2489c75cf34e..28be9c41f0ba 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
@@ -322,6 +322,7 @@ public static Stream decodePathTests()
{"/f%6f%6F/bar", "/foo/bar", EnumSet.noneOf(Violation.class)},
{"/f%u006f%u006F/bar", "/foo/bar", EnumSet.of(Violation.UTF16)},
{"/f%u0001%u0001/bar", "/f\001\001/bar", EnumSet.of(Violation.UTF16)},
+ {"/foo/%u20AC/bar", "/foo/\u20AC/bar", EnumSet.of(Violation.UTF16)},
// illegal paths
{"//host/../path/info", null, EnumSet.noneOf(Violation.class)},
@@ -333,6 +334,9 @@ public static Stream decodePathTests()
{"/path/%u000X/info", null, EnumSet.noneOf(Violation.class)},
{"/path/Fo%u0000/info", null, EnumSet.noneOf(Violation.class)},
{"/path/Fo%00/info", null, EnumSet.noneOf(Violation.class)},
+ {"/path/Foo/info%u0000", null, EnumSet.noneOf(Violation.class)},
+ {"/path/Foo/info%00", null, EnumSet.noneOf(Violation.class)},
+ {"/path/%U20AC", null, EnumSet.noneOf(Violation.class)},
{"%2e%2e/info", null, EnumSet.noneOf(Violation.class)},
{"%u002e%u002e/info", null, EnumSet.noneOf(Violation.class)},
{"%2e%2e;/info", null, EnumSet.noneOf(Violation.class)},
@@ -769,4 +773,20 @@ public void testCompareToJavaNetURI(String input, String scheme, String host, In
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(javaUri.getFragment()));
assertThat("[" + input + "] .toString", httpUri.toString(), is(javaUri.toASCIIString()));
}
+
+ public static Stream queryData()
+ {
+ return Stream.of(
+ new String[]{"/path?p=%U20AC", "p=%U20AC"},
+ new String[]{"/path?p=%u20AC", "p=%u20AC"}
+ ).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("queryData")
+ public void testEncodedQuery(String input, String expectedQuery)
+ {
+ HttpURI httpURI = new HttpURI(input);
+ assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
+ }
}
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java
index d2efd9018989..8108b9d2ac21 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java
@@ -39,6 +39,7 @@
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
@@ -450,12 +451,17 @@ public Resource getResource(String uriInContext) throws MalformedURLException
// If no regular resource exists check for access to /WEB-INF/lib or /WEB-INF/classes
if ((resource == null || !resource.exists()) && uriInContext != null && _classes != null)
{
+ // Canonicalize again to look for the resource inside /WEB-INF subdirectories.
+ String uri = URIUtil.canonicalPath(uriInContext);
+ if (uri == null)
+ return null;
+
try
{
// Replace /WEB-INF/classes with candidates for the classpath
- if (uriInContext.startsWith(WEB_INF_CLASSES_PREFIX))
+ if (uri.startsWith(WEB_INF_CLASSES_PREFIX))
{
- if (uriInContext.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX) || uriInContext.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX + "/"))
+ if (uri.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX) || uri.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX + "/"))
{
//exact match for a WEB-INF/classes, so preferentially return the resource matching the web-inf classes
//rather than the test classes
@@ -471,7 +477,7 @@ else if (_testClasses != null)
int i = 0;
while (res == null && (i < _webInfClasses.size()))
{
- String newPath = StringUtil.replace(uriInContext, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
+ String newPath = StringUtil.replace(uri, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
res = Resource.newResource(newPath);
if (!res.exists())
{
@@ -482,11 +488,11 @@ else if (_testClasses != null)
return res;
}
}
- else if (uriInContext.startsWith(WEB_INF_LIB_PREFIX))
+ else if (uri.startsWith(WEB_INF_LIB_PREFIX))
{
// Return the real jar file for all accesses to
// /WEB-INF/lib/*.jar
- String jarName = StringUtil.strip(uriInContext, WEB_INF_LIB_PREFIX);
+ String jarName = StringUtil.strip(uri, WEB_INF_LIB_PREFIX);
if (jarName.startsWith("/") || jarName.startsWith("\\"))
jarName = jarName.substring(1);
if (jarName.length() == 0)
diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java
index 4754173ca315..43347ca26196 100644
--- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java
+++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectUtil.java
@@ -53,12 +53,12 @@ public static String toRedirectURL(final HttpServletRequest request, String loca
String path = request.getRequestURI();
String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
location = URIUtil.canonicalURI(URIUtil.addEncodedPaths(parent, location));
- if (!location.startsWith("/"))
+ if (location != null && !location.startsWith("/"))
url.append('/');
}
if (location == null)
- throw new IllegalStateException("path cannot be above root");
+ throw new IllegalStateException("redirect path cannot be above root");
url.append(location);
location = url.toString();
diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java
index fff775784b97..b060f29d13ee 100644
--- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java
+++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java
@@ -24,6 +24,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SuppressWarnings("unused")
@@ -75,6 +76,12 @@ public void testInvalidUrlWithReason() throws Exception
@Test
public void testInvalidJsp() throws Exception
+ {
+ assertThrows(IllegalArgumentException.class, () -> _request.setURIPathQuery("/jsp/bean1.jsp%00"));
+ }
+
+ @Test
+ public void testInvalidJspWithNullByte() throws Exception
{
_rule.setCode("405");
_rule.setReason("foo");
@@ -87,6 +94,12 @@ public void testInvalidJsp() throws Exception
assertEquals("foo", _response.getReason());
}
+ @Test
+ public void testInvalidShamrock() throws Exception
+ {
+ assertThrows(IllegalArgumentException.class, () -> _request.setURIPathQuery("/jsp/shamrock-%00%E2%98%98.jsp"));
+ }
+
@Test
public void testValidShamrock() throws Exception
{
@@ -110,4 +123,3 @@ public void testCharacters() throws Exception
//@checkstyle-enable-check : IllegalTokenText
}
}
-
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
index c7c93a6fab66..8b5020dfa1b9 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
@@ -1942,13 +1942,11 @@ public Resource getResource(String path) throws MalformedURLException
if (_baseResource == null)
return null;
- // Does the path go above the current scope?
- path = URIUtil.canonicalPath(path);
- if (path == null)
- return null;
-
try
{
+ // addPath with accept non-canonical paths that don't go above the root,
+ // but will treat them as aliases. So unless allowed by an AliasChecker
+ // they will be rejected below.
Resource resource = _baseResource.addPath(path);
if (checkAlias(path, resource))
@@ -2299,6 +2297,10 @@ else if (path.charAt(0) != '/')
@Override
public URL getResource(String path) throws MalformedURLException
{
+ // This is an API call from the application which may have arbitrary non canonical paths passed
+ // Thus we canonicalize here, to avoid the enforcement of only canonical paths in
+ // ContextHandler.this.getResource(path).
+ path = URIUtil.canonicalPath(path);
if (path == null)
return null;
Resource resource = ContextHandler.this.getResource(path);
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
index 43709a297223..e98f20410d3a 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
@@ -187,7 +187,9 @@ public Resource getResource(String path)
}
}
else if (_context != null)
+ {
r = _context.getResource(path);
+ }
if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
r = getStylesheet();
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
index 9ce41675f850..29ab15308e75 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
@@ -829,6 +829,12 @@ public void testBadUTF8FallsbackTo8859() throws Exception
Log.getLogger(HttpParser.class).info("badMessage: bad encoding expected ...");
String response;
+ response = connector.getResponse("GET /foo/bar%c0%00 HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Connection: close\r\n" +
+ "\r\n");
+ checkContains(response, 0, "HTTP/1.1 400");
+
response = connector.getResponse("GET /bad/utf8%c1 HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
index f3b26d86fd07..a82c14a07e8d 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java
@@ -235,16 +235,23 @@ public void testDoesNotExistResource() throws Exception
@Test
public void testAlias() throws Exception
{
- Resource resource = context.getResource("/./index.html");
- assertNotNull(resource);
- assertFalse(resource.isAlias());
+ String path = "/./index.html";
+ Resource resource = context.getResource(path);
+ assertNull(resource);
+ URL resourceURL = context.getServletContext().getResource(path);
+ assertFalse(resourceURL.getPath().contains("/./"));
- resource = context.getResource("/down/../index.html");
- assertNotNull(resource);
- assertFalse(resource.isAlias());
+ path = "/down/../index.html";
+ resource = context.getResource(path);
+ assertNull(resource);
+ resourceURL = context.getServletContext().getResource(path);
+ assertFalse(resourceURL.getPath().contains("/../"));
- resource = context.getResource("//index.html");
+ path = "//index.html";
+ resource = context.getResource(path);
assertNull(resource);
+ resourceURL = context.getServletContext().getResource(path);
+ assertNull(resourceURL);
}
@ParameterizedTest
diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java
index 02ac7ebaedb1..27f02260f649 100644
--- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java
+++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/RequestURITest.java
@@ -74,7 +74,8 @@ public static Stream data()
ret.add(Arguments.of("/hello%u0025world", "/hello%u0025world", null));
ret.add(Arguments.of("/hello-euro-%E2%82%AC", "/hello-euro-%E2%82%AC", null));
ret.add(Arguments.of("/hello-euro?%E2%82%AC", "/hello-euro", "%E2%82%AC"));
- // test the ascii control characters (just for completeness)
+ // Test the ascii control characters (just for completeness).
+ // Zero is not allowed in UTF-8 sequences so start from 1.
for (int i = 0x1; i < 0x1f; i++)
{
String raw = String.format("/hello%%%02Xworld", i);
@@ -198,7 +199,6 @@ public void testGetRequestURIHTTP10(String rawpath, String expectedReqUri, Strin
// Read the response.
String response = readResponse(client);
- // TODO: is HTTP/1.1 response appropriate for an HTTP/1.0 request?
assertThat(response, Matchers.containsString("HTTP/1.1 200 OK"));
assertThat(response, Matchers.containsString("RequestURI: " + expectedReqUri));
assertThat(response, Matchers.containsString("QueryString: " + expectedQuery));
@@ -225,4 +225,45 @@ public void testGetRequestURIHTTP11(String rawpath, String expectedReqUri, Strin
assertThat(response, Matchers.containsString("QueryString: " + expectedQuery));
}
}
+
+ public static Stream badData()
+ {
+ List ret = new ArrayList<>();
+ ret.add(Arguments.of("/hello\000"));
+ ret.add(Arguments.of("/hello%00"));
+ ret.add(Arguments.of("/hello%u0000"));
+ ret.add(Arguments.of("/hello\000/world"));
+ ret.add(Arguments.of("/hello%00world"));
+ ret.add(Arguments.of("/hello%u0000world"));
+ ret.add(Arguments.of("/hello%GG"));
+ ret.add(Arguments.of("/hello%;/world"));
+ ret.add(Arguments.of("/hello/../../world"));
+ ret.add(Arguments.of("/hello/..;/world"));
+ ret.add(Arguments.of("/hello/..;?/world"));
+ ret.add(Arguments.of("/hello/%#x/../world"));
+ ret.add(Arguments.of("/../hello/world"));
+ ret.add(Arguments.of("/hello%u00u00/world"));
+ ret.add(Arguments.of("hello"));
+
+ return ret.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("badData")
+ public void testGetBadRequestsURIHTTP10(String rawpath) throws Exception
+ {
+ try (Socket client = newSocket(serverURI.getHost(), serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ String request = String.format("GET %s HTTP/1.0\r\n\r\n", rawpath);
+ os.write(request.getBytes(StandardCharsets.ISO_8859_1));
+ os.flush();
+
+ // Read the response.
+ String response = readResponse(client);
+
+ assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
+ }
+ }
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
index 3c6f7a53d343..6817072ad721 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
@@ -537,7 +537,6 @@ public static String decodePath(String path, int offset, int length)
{
throw new IllegalArgumentException("cannot decode URI", e);
}
-
}
/* Decode a URI path and strip parameters of ISO-8859-1 path
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
index 40ead6f17972..c3d77b329c42 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileResource.java
@@ -270,8 +270,11 @@ public Resource addPath(String path)
{
assertValidPath(path);
- if (path == null)
- throw new MalformedURLException();
+ // Check that the path is within the root,
+ // but use the original path to create the
+ // resource, to preserve aliasing.
+ if (URIUtil.canonicalPath(path) == null)
+ throw new MalformedURLException(path);
if ("/".equals(path))
return this;
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
index 22f936a62cfd..3c58739016b6 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
@@ -94,8 +94,11 @@ private Path checkAliasPath()
if (!abs.isAbsolute())
abs = path.toAbsolutePath();
+ // Any normalization difference means it's an alias,
+ // and we don't want to bother further to follow
+ // symlinks as it's an alias anyway.
Path normal = path.normalize();
- if (!abs.equals(normal))
+ if (!isSameName(abs, normal))
return normal;
try
@@ -105,11 +108,8 @@ private Path checkAliasPath()
if (Files.exists(path))
{
Path real = abs.toRealPath(FOLLOW_LINKS);
-
if (!isSameName(abs, real))
- {
return real;
- }
}
}
catch (IOException e)
@@ -361,20 +361,23 @@ public boolean isSame(Resource resource)
}
@Override
- public Resource addPath(final String subpath) throws IOException
+ public Resource addPath(final String subPath) throws IOException
{
- if ((subpath == null) || (subpath.length() == 0))
- throw new MalformedURLException(subpath);
+ // Check that the path is within the root,
+ // but use the original path to create the
+ // resource, to preserve aliasing.
+ if (URIUtil.canonicalPath(subPath) == null)
+ throw new MalformedURLException(subPath);
- if ("/".equals(subpath))
+ if ("/".equals(subPath))
return this;
- // subpaths are always under PathResource
- // compensate for input subpaths like "/subdir"
+ // Sub-paths are always under PathResource
+ // compensate for input sub-paths like "/subdir"
// where default resolve behavior would be
// to treat that like an absolute path
- return new PathResource(this, subpath);
+ return new PathResource(this, subPath);
}
private void assertValidPath(Path path)
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
index dc443becffb3..d3a32b5d69fc 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java
@@ -459,10 +459,12 @@ public abstract boolean renameTo(Resource dest)
* Returns the resource contained inside the current resource with the
* given name.
*
- * @param path The path segment to add, which is not encoded
+ * @param path The path segment to add, which is not encoded. The path may be non canonical, but if so then
+ * the resulting Resource will return true from {@link #isAlias()}.
* @return the Resource for the resolved path within this Resource.
* @throws IOException if unable to resolve the path
- * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed.
+ * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed, or
+ * a relative path attempts to access above the root resource.
*/
public abstract Resource addPath(String path)
throws IOException, MalformedURLException;
@@ -555,6 +557,8 @@ public String getListHTML(String base, boolean parent) throws IOException
*/
public String getListHTML(String base, boolean parent, String query) throws IOException
{
+ // This method doesn't check aliases, so it is OK to canonicalize here.
+ base = URIUtil.canonicalPath(base);
if (base == null || !isDirectory())
return null;
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
index 054f4d0d5ab6..40d68abdf56c 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java
@@ -271,8 +271,11 @@ public String[] list()
public Resource addPath(String path)
throws IOException
{
- if (path == null)
- throw new MalformedURLException("null path");
+ // Check that the path is within the root,
+ // but use the original path to create the
+ // resource, to preserve aliasing.
+ if (URIUtil.canonicalPath(path) == null)
+ throw new MalformedURLException(path);
return newResource(URIUtil.addEncodedPaths(_url.toExternalForm(), URIUtil.encodePath(path)), _useCaches);
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
index c40ff6a83c7e..a52a64444d2a 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java
@@ -151,7 +151,9 @@ public void testCanonicalPath(String input, String expectedResult)
// Check canonicalURI
if (expectedResult == null)
+ {
assertThat(URIUtil.canonicalURI(input), nullValue());
+ }
else
{
// mostly encodedURI will be the same
@@ -162,4 +164,22 @@ public void testCanonicalPath(String input, String expectedResult)
}
}
+ public static Stream queries()
+ {
+ String[][] data =
+ {
+ {"/ctx/../dir?/../index.html", "/dir?/../index.html"},
+ {"/get-files?file=/etc/passwd", "/get-files?file=/etc/passwd"},
+ {"/get-files?file=../../../../../passwd", "/get-files?file=../../../../../passwd"}
+ };
+ return Stream.of(data).map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("queries")
+ public void testQuery(String input, String expectedPath)
+ {
+ String actual = URIUtil.canonicalURI(input);
+ assertThat(actual, is(expectedPath));
+ }
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
index c93dc731ed74..5d7c7b29dc5a 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java
@@ -21,6 +21,7 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
@@ -45,6 +46,8 @@
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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class ResourceTest
{
@@ -325,4 +328,19 @@ public void testEqualsWindowsCaseInsensitiveDrive() throws Exception
assertEquals(rb, ra);
}
+
+ @Test
+ public void testClimbAboveBase() throws Exception
+ {
+ Resource resource = Resource.newResource("/foo/bar");
+ assertThrows(MalformedURLException.class, () -> resource.addPath(".."));
+
+ Resource same = resource.addPath(".");
+ assertNotNull(same);
+ assertTrue(same.isAlias());
+
+ assertThrows(MalformedURLException.class, () -> resource.addPath("./.."));
+
+ assertThrows(MalformedURLException.class, () -> resource.addPath("./../bar"));
+ }
}