From 92a74d6734881cf067f54e71bf5188dc7fdbbd52 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Mon, 28 Jun 2021 15:52:14 +1000
Subject: [PATCH 1/6] Issue #6473 - canonicalPath refactor & fix alias check in
PathResource
Signed-off-by: Lachlan Roberts
---
.../java/org/eclipse/jetty/http/HttpURI.java | 154 +++++++++--
.../org/eclipse/jetty/http/UriCompliance.java | 58 +++-
.../org/eclipse/jetty/http/HttpURITest.java | 132 +++++-----
.../jetty/rewrite/handler/RedirectUtil.java | 4 +-
.../rewrite/handler/ValidUrlRuleTest.java | 17 +-
.../org/eclipse/jetty/server/Dispatcher.java | 34 ++-
.../org/eclipse/jetty/server/Request.java | 33 +--
.../org/eclipse/jetty/server/Response.java | 4 +-
.../jetty/server/handler/ContextHandler.java | 21 +-
.../jetty/server/handler/ResourceHandler.java | 1 -
.../jetty/server/HttpConnectionTest.java | 12 +-
.../org/eclipse/jetty/server/RequestTest.java | 9 -
.../ContextHandlerGetResourceTest.java | 34 ++-
.../eclipse/jetty/servlet/DefaultServlet.java | 4 +-
.../eclipse/jetty/servlet/RequestURITest.java | 4 +-
.../servlets/DataRateLimitedServlet.java | 7 +-
.../org/eclipse/jetty/servlets/PutFilter.java | 8 +-
.../java/org/eclipse/jetty/util/URIUtil.java | 248 +++++++++---------
.../eclipse/jetty/util/Utf8Appendable.java | 1 -
.../jetty/util/resource/PathResource.java | 20 +-
.../eclipse/jetty/util/resource/Resource.java | 1 -
.../jetty/util/resource/URLResource.java | 5 +-
.../jetty/util/URIUtilCanonicalPathTest.java | 27 +-
.../jetty/util/Utf8AppendableTest.java | 29 ++
.../eclipse/jetty/webapp/WebAppContext.java | 15 +-
.../jetty/webapp/WebAppContextTest.java | 40 +++
.../jsp/JspAndDefaultWithoutAliasesTest.java | 37 ++-
27 files changed, 608 insertions(+), 351 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 42f829068e9c..596f1acc1424 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
@@ -15,7 +15,10 @@
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
import java.util.EnumSet;
+import java.util.Set;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index;
@@ -31,54 +34,92 @@
* via the static methods such as {@link #build()} and {@link #from(String)}.
*
* A URI such as
- * http://user@host:port/path;ignored/info;param?query#ignored
- * is split into the following undecoded elements:
+ * 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
* - {@link #getHost()} - host
* - {@link #getPort()} - port
- * - {@link #getPath()} - /path/info
- * - {@link #getParam()} - param
+ * - {@link #getPath()} - /path;param1/%2e/info;param2
+ * - {@link #getDecodedPath()} - /path/info
+ * - {@link #getParam()} - param2
* - {@link #getQuery()} - query
* - {@link #getFragment()} - fragment
*
- * Any parameters will be returned from {@link #getPath()}, but are excluded from the
- * return value of {@link #getDecodedPath()}. If there are multiple parameters, the
- * {@link #getParam()} method returns only the last one.
- */
+ *
The path part of the URI is provided in both raw form ({@link #getPath()}) and
+ * decoded form ({@link #getDecodedPath}), which has: path parameters removed,
+ * percent encoded characters expanded and relative segments resolved. This approach
+ * is somewhat contrary to RFC3986
+ * which no longer defines path parameters (removed after
+ * RFC2396) and specifies
+ * that relative segment normalization should take place before percent encoded character
+ * expansion. A literal interpretation of the RFC can result in URI paths with ambiguities
+ * when viewed as strings. For example, a URI of {@code /foo%2f..%2fbar} is technically a single
+ * segment of "/foo/../bar", but could easily be misinterpreted as 3 segments resolving to "/bar"
+ * by a file system.
+ *
+ *
+ * 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
+ * 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()}.
+ *
+ *
+ * If there are multiple path parameters, only the last one is returned by {@link #getParam()}.
+ *
+ **/
public interface HttpURI
{
+ /**
+ * Violations of safe URI interpretations
+ */
enum Violation
{
/**
- * URI contains ambiguous path segments e.g. {@code /foo/%2e%2e/bar}
+ * Ambiguous path segments e.g. /foo/%2E%2E/bar
*/
- SEGMENT,
+ SEGMENT("Ambiguous path segments"),
/**
- * URI contains ambiguous empty segments e.g. {@code /foo//bar} or {@code /foo/;param/}, but not {@code /foo/}
+ * Ambiguous path separator within a URI segment e.g. /foo%2Fbar
*/
- EMPTY,
+ SEPARATOR("Ambiguous path separator"),
/**
- * URI contains ambiguous path separator within a URI segment e.g. {@code /foo/b%2fr}
+ * Ambiguous path parameters within a URI segment e.g. /foo/..;/bar
*/
- SEPARATOR,
+ PARAM("Ambiguous path parameters"),
/**
- * URI contains ambiguous path encoding within a URI segment e.g. {@code /%2557EB-INF}
+ * Ambiguous double encoding within a URI segment e.g. /%2557EB-INF
*/
- ENCODING,
+ ENCODING("Ambiguous double encoding"),
/**
- * URI contains ambiguous path parameters within a URI segment e.g. {@code /foo/..;/bar}
+ * Ambiguous empty segments e.g. /foo//bar
*/
- PARAM,
+ EMPTY("Ambiguous empty segments"),
/**
- * Contains UTF16 encodings
+ * Non standard UTF-16 encoding eg /foo%u2192bar
.
*/
- UTF16
+ UTF16("Non standard UTF-16 encoding");
+
+ private final String _message;
+
+ Violation(String message)
+ {
+ _message = message;
+ }
+
+ String getMessage()
+ {
+ return _message;
+ }
}
static Mutable build()
@@ -135,6 +176,11 @@ static Immutable from(String scheme, String host, int port, String pathQuery)
return new Mutable(scheme, host, port, pathQuery).asImmutable();
}
+ static Immutable build(HttpURI schemeHostPort, HttpURI uri)
+ {
+ return new Immutable(schemeHostPort, uri);
+ }
+
Immutable asImmutable();
String asString();
@@ -147,6 +193,11 @@ static Immutable from(String scheme, String host, int port, String pathQuery)
String getHost();
+ /**
+ * Get a URI path parameter. Multiple and in segment parameters are ignored and only
+ * the last trailing parameter is returned.
+ * @return The last path parameter or null
+ */
String getParam();
String getPath();
@@ -175,6 +226,17 @@ static Immutable from(String scheme, String host, int port, String pathQuery)
*/
boolean hasViolations();
+ /**
+ * @param violation the violation to check.
+ * @return true if the URI has the passed violation.
+ */
+ boolean hasViolation(Violation violation);
+
+ /**
+ * @return Set of violations in the URI.
+ */
+ Collection getViolations();
+
/**
* @return True if the URI has a possibly ambiguous segment like '..;' or '%2e%2e'
*/
@@ -258,6 +320,22 @@ private Immutable(String uri)
_decodedPath = null;
}
+ private Immutable(HttpURI schemeHostPort, HttpURI uri)
+ {
+ _scheme = schemeHostPort.getScheme();
+ _user = schemeHostPort.getUser();
+ _host = schemeHostPort.getHost();
+ _port = schemeHostPort.getPort();
+ _path = uri.getPath();
+ _param = uri.getParam();
+ _query = uri.getQuery();
+ _fragment = uri.getFragment();
+ _uri = null;
+ _decodedPath = uri.getDecodedPath();
+ if (uri.hasViolations())
+ _violations.addAll(uri.getViolations());
+ }
+
@Override
public Immutable asImmutable()
{
@@ -417,6 +495,18 @@ public boolean hasViolations()
return !_violations.isEmpty();
}
+ @Override
+ public boolean hasViolation(Violation violation)
+ {
+ return _violations.contains(violation);
+ }
+
+ @Override
+ public Collection getViolations()
+ {
+ return Collections.unmodifiableSet(_violations);
+ }
+
@Override
public boolean hasAmbiguousSegment()
{
@@ -790,6 +880,18 @@ public boolean hasViolations()
return !_violations.isEmpty();
}
+ @Override
+ public boolean hasViolation(Violation violation)
+ {
+ return _violations.contains(violation);
+ }
+
+ @Override
+ public Collection getViolations()
+ {
+ return Collections.unmodifiableSet(_violations);
+ }
+
@Override
public boolean hasAmbiguousSegment()
{
@@ -1216,6 +1318,8 @@ else if (c == '/')
{
switch (encodedValue)
{
+ case 0:
+ throw new IllegalArgumentException("Illegal character in path");
case '/':
_violations.add(Violation.SEPARATOR);
break;
@@ -1387,10 +1491,12 @@ else if (c == '/')
}
else if (_path != null)
{
- String canonical = URIUtil.canonicalPath(_path);
- if (canonical == null)
- throw new BadMessageException("Bad URI");
- _decodedPath = URIUtil.decodePath(canonical);
+ // 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);
+ if (_decodedPath == null)
+ throw new IllegalArgumentException("Bad URI");
}
}
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 f9d0504283ab..b82b13c7e5a5 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
@@ -14,6 +14,7 @@
package org.eclipse.jetty.http;
import java.util.Arrays;
+import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
@@ -67,10 +68,6 @@ public enum Violation implements ComplianceViolation
* 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
- */
- NON_CANONICAL_AMBIGUOUS_PATHS("https://tools.ietf.org/html/rfc3986#section-3.3", "Non canonical ambiguous paths"),
/**
* Allow UTF-16 encoding eg /foo%u2192bar
.
*/
@@ -125,10 +122,9 @@ public String getDescription()
/**
* Compliance mode that exactly follows RFC3986,
- * including allowing all additional ambiguous URI Violations,
- * except {@link Violation#NON_CANONICAL_AMBIGUOUS_PATHS}, thus ambiguous paths are canonicalized for safety.
+ * including allowing all additional ambiguous URI Violations.
*/
- public static final UriCompliance RFC3986 = new UriCompliance("RFC3986", complementOf(of(Violation.NON_CANONICAL_AMBIGUOUS_PATHS)));
+ public static final UriCompliance RFC3986 = new UriCompliance("RFC3986", allOf(Violation.class));
/**
* Compliance mode that follows RFC3986
@@ -231,6 +227,11 @@ public static UriCompliance from(String spec)
boolean exclude = element.startsWith("-");
if (exclude)
element = element.substring(1);
+
+ // Ignore removed name. TODO: remove in future release.
+ if (element.equals("NON_CANONICAL_AMBIGUOUS_PATHS"))
+ continue;
+
Violation section = Violation.valueOf(element);
if (exclude)
violations.remove(section);
@@ -330,4 +331,47 @@ private static Set copyOf(Set violations)
return EnumSet.noneOf(Violation.class);
return EnumSet.copyOf(violations);
}
+
+ private static final EnumMap __uriViolations = new EnumMap<>(HttpURI.Violation.class);
+
+ static
+ {
+ // create a map from Violation to compliance in a loop, so that any new violations added are detected with ISE
+ for (HttpURI.Violation violation : HttpURI.Violation.values())
+ {
+ switch (violation)
+ {
+ case SEPARATOR:
+ __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_SEPARATOR);
+ break;
+ case SEGMENT:
+ __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_SEGMENT);
+ break;
+ case PARAM:
+ __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_PARAMETER);
+ break;
+ case ENCODING:
+ __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_ENCODING);
+ break;
+ case EMPTY:
+ __uriViolations.put(violation, Violation.AMBIGUOUS_EMPTY_SEGMENT);
+ break;
+ case UTF16:
+ __uriViolations.put(violation, Violation.UTF16_ENCODINGS);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ public static String checkUriCompliance(UriCompliance compliance, HttpURI uri)
+ {
+ for (HttpURI.Violation violation : HttpURI.Violation.values())
+ {
+ if (uri.hasViolation(violation) && (compliance == null || !compliance.allows(__uriViolations.get(violation))))
+ return violation.getMessage();
+ }
+ return null;
+ }
}
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 4c199e15d40b..45e20953fa10 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
@@ -146,6 +146,12 @@ public void testParse()
uri = builder.asImmutable();
assertThat(uri.getHost(), is("foo"));
assertThat(uri.getPath(), is("/bar"));
+
+ // We do allow nulls if not encoded. This can be used for testing 2nd line of defence.
+ builder.uri("http://fo\000/bar");
+ uri = builder.asImmutable();
+ assertThat(uri.getHost(), is("fo\000"));
+ assertThat(uri.getPath(), is("/bar"));
}
@Test
@@ -351,6 +357,7 @@ public static Stream decodePathTests()
// encoded paths
{"/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)},
// illegal paths
{"//host/../path/info", null, EnumSet.noneOf(Violation.class)},
@@ -360,32 +367,37 @@ public static Stream decodePathTests()
{"/path/%2/F/info", null, EnumSet.noneOf(Violation.class)},
{"/path/%/info", null, EnumSet.noneOf(Violation.class)},
{"/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)},
+ {"%2e%2e/info", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%u002e/info", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%2e;/info", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%u002e;/info", null, EnumSet.noneOf(Violation.class)},
+ {"%2e.", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e.", null, EnumSet.noneOf(Violation.class)},
+ {".%2e", null, EnumSet.noneOf(Violation.class)},
+ {".%u002e", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%2e", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%u002e", null, EnumSet.noneOf(Violation.class)},
+ {"%2e%u002e", null, EnumSet.noneOf(Violation.class)},
+ {"%u002e%2e", null, EnumSet.noneOf(Violation.class)},
+ {"..;/info", null, EnumSet.noneOf(Violation.class)},
+ {"..;param/info", null, EnumSet.noneOf(Violation.class)},
// ambiguous dot encodings
- {"scheme://host/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
- {"scheme:/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
- {"path/%2e/info/", "path/./info/", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param;other/info;other", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"%2e/info", "./info", EnumSet.of(Violation.SEGMENT)},
- {"%u002e/info", "./info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%2e%2e/info", "../info", EnumSet.of(Violation.SEGMENT)},
- {"%u002e%u002e/info", "../info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%2e%2e;/info", "../info", EnumSet.of(Violation.SEGMENT)},
- {"%u002e%u002e;/info", "../info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%2e", ".", EnumSet.of(Violation.SEGMENT)},
- {"%u002e", ".", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%2e.", "..", EnumSet.of(Violation.SEGMENT)},
- {"%u002e.", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {".%2e", "..", EnumSet.of(Violation.SEGMENT)},
- {".%u002e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%2e%2e", "..", EnumSet.of(Violation.SEGMENT)},
- {"%u002e%u002e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%2e%u002e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
- {"%u002e%2e", "..", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
+ {"scheme://host/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"scheme:/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"path/%2e/info/", "path/info/", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"%2e/info", "info", EnumSet.of(Violation.SEGMENT)},
+ {"%u002e/info", "info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
+
+ {"%2e", "", EnumSet.of(Violation.SEGMENT)},
+ {"%u002e", "", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
// empty segment treated as ambiguous
{"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
@@ -403,20 +415,18 @@ public static Stream decodePathTests()
{"http:/foo", "/foo", EnumSet.noneOf(Violation.class)},
// ambiguous parameter inclusions
- {"/path/.;/info", "/path/./info", EnumSet.of(Violation.PARAM)},
- {"/path/.;param/info", "/path/./info", EnumSet.of(Violation.PARAM)},
- {"/path/..;/info", "/path/../info", EnumSet.of(Violation.PARAM)},
- {"/path/..;param/info", "/path/../info", EnumSet.of(Violation.PARAM)},
- {".;/info", "./info", EnumSet.of(Violation.PARAM)},
- {".;param/info", "./info", EnumSet.of(Violation.PARAM)},
- {"..;/info", "../info", EnumSet.of(Violation.PARAM)},
- {"..;param/info", "../info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;param/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;/info", "/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;param/info", "/info", EnumSet.of(Violation.PARAM)},
+ {".;/info", "info", EnumSet.of(Violation.PARAM)},
+ {".;param/info", "info", EnumSet.of(Violation.PARAM)},
// ambiguous segment separators
{"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
{"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
{"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
- {"/path/%2f../info", "/path//../info", EnumSet.of(Violation.SEPARATOR)},
+ {"/path/%2f../info", "/path/info", EnumSet.of(Violation.SEPARATOR)},
// ambiguous encoding
{"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
@@ -426,9 +436,9 @@ public static Stream decodePathTests()
{"/path/%u0025../info", "/path/%../info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
// combinations
- {"/path/%2f/..;/info", "/path///../info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
- {"/path/%u002f/..;/info", "/path///../info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.UTF16)},
- {"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
+ {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
+ {"/path/%u002f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.UTF16)},
+ {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
// Non ascii characters
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
@@ -483,21 +493,23 @@ public static Stream testPathQueryTests()
{"../path/info", null, null},
{"/path/%XX/info", null, null},
{"/path/%2/F/info", null, null},
+ {"%2e%2e/info", null, null},
+ {"%2e%2e;/info", null, null},
+ {"%2e.", null, null},
+ {".%2e", null, null},
+ {"%2e%2e", null, null},
+ {"..;/info", null, null},
+ {"..;param/info", null, null},
// ambiguous dot encodings
- {"/path/%2e/info", "/path/./info", EnumSet.of(Violation.SEGMENT)},
- {"path/%2e/info/", "path/./info/", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param/info", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param;other/info;other", "/path/../info", EnumSet.of(Violation.SEGMENT)},
- {"%2e/info", "./info", EnumSet.of(Violation.SEGMENT)},
- {"%2e%2e/info", "../info", EnumSet.of(Violation.SEGMENT)},
- {"%2e%2e;/info", "../info", EnumSet.of(Violation.SEGMENT)},
- {"%2e", ".", EnumSet.of(Violation.SEGMENT)},
- {"%2e.", "..", EnumSet.of(Violation.SEGMENT)},
- {".%2e", "..", EnumSet.of(Violation.SEGMENT)},
- {"%2e%2e", "..", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
+ {"path/%2e/info/", "path/info/", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.SEGMENT)},
+ {"%2e/info", "info", EnumSet.of(Violation.SEGMENT)},
+ {"%2e", "", EnumSet.of(Violation.SEGMENT)},
// empty segment treated as ambiguous
{"/", "/", EnumSet.noneOf(Violation.class)},
@@ -531,20 +543,18 @@ public static Stream testPathQueryTests()
{"", "", EnumSet.noneOf(Violation.class)},
// ambiguous parameter inclusions
- {"/path/.;/info", "/path/./info", EnumSet.of(Violation.PARAM)},
- {"/path/.;param/info", "/path/./info", EnumSet.of(Violation.PARAM)},
- {"/path/..;/info", "/path/../info", EnumSet.of(Violation.PARAM)},
- {"/path/..;param/info", "/path/../info", EnumSet.of(Violation.PARAM)},
- {".;/info", "./info", EnumSet.of(Violation.PARAM)},
- {".;param/info", "./info", EnumSet.of(Violation.PARAM)},
- {"..;/info", "../info", EnumSet.of(Violation.PARAM)},
- {"..;param/info", "../info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;param/info", "/path/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;/info", "/info", EnumSet.of(Violation.PARAM)},
+ {"/path/..;param/info", "/info", EnumSet.of(Violation.PARAM)},
+ {".;/info", "info", EnumSet.of(Violation.PARAM)},
+ {".;param/info", "info", EnumSet.of(Violation.PARAM)},
// ambiguous segment separators
{"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
{"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
{"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
- {"/path/%2f../info", "/path//../info", EnumSet.of(Violation.SEPARATOR)},
+ {"/path/%2f../info", "/path/info", EnumSet.of(Violation.SEPARATOR)},
// ambiguous encoding
{"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
@@ -552,9 +562,9 @@ public static Stream testPathQueryTests()
{"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
// combinations
- {"/path/%2f/..;/info", "/path///../info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
- {"/path/%2f/..;/%2e/info", "/path///.././info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
- {"/path/%2f/%25/..;/%2e//info", "/path///%/.././/info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT, Violation.ENCODING, Violation.EMPTY)},
+ {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
+ {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
+ {"/path/%2f/%25/..;/%2e//info", "/path////info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT, Violation.ENCODING, Violation.EMPTY)},
}).map(Arguments::of);
}
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 2f4e5b693053..058dac8293da 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
@@ -40,14 +40,14 @@ public static String toRedirectURL(final HttpServletRequest request, String loca
if (location.startsWith("/"))
{
// absolute in context
- location = URIUtil.canonicalEncodedPath(location);
+ location = URIUtil.canonicalURI(location);
}
else
{
// relative to request
String path = request.getRequestURI();
String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
- location = URIUtil.canonicalPath(URIUtil.addEncodedPaths(parent, location));
+ location = URIUtil.canonicalURI(URIUtil.addEncodedPaths(parent, location));
if (!location.startsWith("/"))
url.append('/');
}
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 fbe12b887a80..363aedb3ad0c 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
@@ -16,7 +16,6 @@
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Dispatcher;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -62,7 +61,7 @@ public void testInvalidUrlWithMessage() throws Exception
{
_rule.setCode("405");
_rule.setMessage("foo");
- _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/%00/"));
+ _request.setHttpURI(HttpURI.from("/%01/"));
String result = _rule.matchAndApply(_request.getRequestURI(), _request, _response);
@@ -75,20 +74,8 @@ public void testInvalidJsp() throws Exception
{
_rule.setCode("405");
_rule.setMessage("foo");
- _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/jsp/bean1.jsp%00"));
- String result = _rule.matchAndApply(_request.getRequestURI(), _request, _response);
-
- assertEquals(405, _response.getStatus());
- assertEquals("foo", _request.getAttribute(Dispatcher.ERROR_MESSAGE));
- }
-
- @Test
- public void testInvalidShamrock() throws Exception
- {
- _rule.setCode("405");
- _rule.setMessage("foo");
- _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/jsp/shamrock-%00%E2%98%98.jsp"));
+ _request.setHttpURI(HttpURI.from("/jsp/bean1.jsp\000"));
String result = _rule.matchAndApply(_request.getRequestURI(), _request, _response);
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
index fcd2dcef43db..44a8dfc7eb43 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
@@ -15,6 +15,7 @@
import java.io.IOException;
import java.util.HashSet;
+import java.util.Objects;
import java.util.Set;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
@@ -26,7 +27,10 @@
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.UriCompliance;
+import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.MultiMap;
@@ -76,7 +80,7 @@ public void error(ServletRequest request, ServletResponse response) throws Servl
@Override
public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException
{
- Request baseRequest = Request.getBaseRequest(request);
+ Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
if (!(request instanceof HttpServletRequest))
request = new ServletRequestHttpWrapper(request);
@@ -98,6 +102,10 @@ public void include(ServletRequest request, ServletResponse response) throws Ser
}
else
{
+ Objects.requireNonNull(_uri);
+ // Check any URI violations against the compliance for this request
+ checkUriViolations(_uri, baseRequest);
+
IncludeAttributes attr = new IncludeAttributes(
old_attr,
baseRequest,
@@ -131,7 +139,7 @@ public void forward(ServletRequest request, ServletResponse response) throws Ser
protected void forward(ServletRequest request, ServletResponse response, DispatcherType dispatch) throws ServletException, IOException
{
- Request baseRequest = Request.getBaseRequest(request);
+ Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
Response baseResponse = baseRequest.getResponse();
baseResponse.resetForForward();
@@ -159,6 +167,10 @@ protected void forward(ServletRequest request, ServletResponse response, Dispatc
}
else
{
+ Objects.requireNonNull(_uri);
+ // Check any URI violations against the compliance for this request
+ checkUriViolations(_uri, baseRequest);
+
// If we have already been forwarded previously, then keep using the established
// original value. Otherwise, this is the first forward and we need to establish the values.
// Note: the established value on the original request for pathInfo and
@@ -173,11 +185,7 @@ protected void forward(ServletRequest request, ServletResponse response, Dispatc
source_mapping,
old_uri.getQuery()));
- String query = _uri.getQuery();
- if (query == null)
- query = old_uri.getQuery();
-
- baseRequest.setHttpURI(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query));
+ baseRequest.setHttpURI(HttpURI.build(old_uri, _uri));
baseRequest.setContext(_contextHandler.getServletContext(), _pathInContext);
baseRequest.setServletPathMapping(null);
@@ -230,6 +238,18 @@ protected void forward(ServletRequest request, ServletResponse response, Dispatc
}
}
+ private static void checkUriViolations(HttpURI uri, Request baseRequest)
+ {
+ if (uri.hasViolations())
+ {
+ HttpChannel channel = baseRequest.getHttpChannel();
+ UriCompliance compliance = channel == null || channel.getHttpConfiguration() == null ? null : channel.getHttpConfiguration().getUriCompliance();
+ String illegalState = UriCompliance.checkUriCompliance(compliance, uri);
+ if (illegalState != null)
+ throw new IllegalStateException(illegalState);
+ }
+ }
+
@Override
public String toString()
{
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 7c05e6239826..dc0a456a4976 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
@@ -67,6 +67,7 @@
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HostPortHttpField;
+import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
import org.eclipse.jetty.http.HttpField;
@@ -1687,28 +1688,13 @@ public void setMetaData(MetaData.Request request)
_method = request.getMethod();
_httpFields = request.getFields();
final HttpURI uri = request.getURI();
- boolean ambiguous = false;
UriCompliance compliance = null;
if (uri.hasViolations())
{
- ambiguous = uri.isAmbiguous();
compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance();
- if (uri.hasUtf16Encoding() && (compliance == null || !compliance.allows(UriCompliance.Violation.UTF16_ENCODINGS)))
- throw new BadMessageException("UTF16 % encoding not supported");
-
- if (ambiguous)
- {
- if (uri.hasAmbiguousSegment() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT)))
- throw new BadMessageException("Ambiguous segment in URI");
- if (uri.hasAmbiguousEmptySegment() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_EMPTY_SEGMENT)))
- throw new BadMessageException("Ambiguous empty segment in URI");
- if (uri.hasAmbiguousSeparator() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR)))
- 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");
- }
+ String badMessage = UriCompliance.checkUriCompliance(compliance, uri);
+ if (badMessage != null)
+ throw new BadMessageException(badMessage);
}
if (uri.isAbsolute() && uri.hasAuthority() && uri.getPath() != null)
@@ -1740,7 +1726,6 @@ public void setMetaData(MetaData.Request request)
}
_uri = builder.asImmutable();
}
-
setSecure(HttpScheme.HTTPS.is(_uri.getScheme()));
String encoded = _uri.getPath();
@@ -1751,17 +1736,15 @@ public void setMetaData(MetaData.Request request)
else if (encoded.startsWith("/"))
{
path = (encoded.length() == 1) ? "/" : _uri.getDecodedPath();
- // Strictly speaking if a URI is legal and encodes ambiguous segments, then they should be
- // reflected in the decoded string version. However, it can be ambiguous to provide a decoded path as
- // a string, so we normalize again. If an application wishes to see ambiguous URIs, then they must
- // set the {@link UriCompliance.Violation#NON_CANONICAL_AMBIGUOUS_PATHS} compliance.
- if (ambiguous && (compliance == null || !compliance.allows(UriCompliance.Violation.NON_CANONICAL_AMBIGUOUS_PATHS)))
- path = URIUtil.canonicalPath(path);
}
else if ("*".equals(encoded) || HttpMethod.CONNECT.is(getMethod()))
+ {
path = encoded;
+ }
else
+ {
path = null;
+ }
if (path == null || path.isEmpty())
{
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
index d79be8e96b04..e2adfed9bd63 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
@@ -568,14 +568,14 @@ public void sendRedirect(int code, String location, boolean consumeAll) throws I
if (location.startsWith("/"))
{
// absolute in context
- location = URIUtil.canonicalEncodedPath(location);
+ location = URIUtil.canonicalURI(location);
}
else
{
// relative to request
String path = _channel.getRequest().getRequestURI();
String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
- location = URIUtil.canonicalEncodedPath(URIUtil.addEncodedPaths(parent, location));
+ location = URIUtil.canonicalURI(URIUtil.addEncodedPaths(parent, location));
if (location != null && !location.startsWith("/"))
buf.append('/');
}
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 91d4d3e665cf..6fe16df2bfb6 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
@@ -29,7 +29,6 @@
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -1490,7 +1489,6 @@ public boolean isProtectedTarget(String target)
return false;
if (target.startsWith("//"))
- // ignore empty segments which may be discard by file system
target = URIUtil.compactPath(target);
ProtectedTargetType type = _protectedTargets.getBest(target);
@@ -1928,9 +1926,13 @@ 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
{
- path = URIUtil.canonicalPath(path);
Resource resource = _baseResource.addPath(path);
if (checkAlias(path, resource))
@@ -1959,9 +1961,8 @@ public boolean checkAlias(String path, Resource resource)
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
// alias checks
- for (Iterator i = getAliasChecks().iterator(); i.hasNext(); )
+ for (AliasCheck check : getAliasChecks())
{
- AliasCheck check = i.next();
if (check.check(path, resource))
{
if (LOG.isDebugEnabled())
@@ -2014,7 +2015,6 @@ public Set getResourcePaths(String path)
{
try
{
- path = URIUtil.canonicalPath(path);
Resource resource = getResource(path);
if (resource != null && resource.exists())
@@ -2214,6 +2214,7 @@ public String getMimeType(String file)
@Override
public RequestDispatcher getRequestDispatcher(String uriInContext)
{
+ // uriInContext is encoded, potentially with query.
if (uriInContext == null)
return null;
@@ -2223,10 +2224,8 @@ public RequestDispatcher getRequestDispatcher(String uriInContext)
try
{
String contextPath = getContextPath();
- String pathInfo;
-
HttpURI.Mutable uri = HttpURI.build(uriInContext);
- pathInfo = URIUtil.canonicalPath(uri.getDecodedPath());
+ String pathInfo = uri.getDecodedPath();
if (StringUtil.isEmpty(pathInfo))
return null;
@@ -2275,6 +2274,8 @@ else if (path.charAt(0) != '/')
@Override
public URL getResource(String path) throws MalformedURLException
{
+ if (path == null)
+ return null;
Resource resource = ContextHandler.this.getResource(path);
if (resource != null && resource.exists())
return resource.getURI().toURL();
@@ -2305,6 +2306,8 @@ public InputStream getResourceAsStream(String path)
@Override
public Set getResourcePaths(String path)
{
+ if (path == null)
+ return null;
return ContextHandler.this.getResourcePaths(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 bbbb960678a1..5a571fada0f2 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
@@ -156,7 +156,6 @@ public Resource getResource(String path) throws IOException
if (_baseResource != null)
{
- path = URIUtil.canonicalPath(path);
r = _baseResource.addPath(path);
if (r.isAlias() && (_context == null || !_context.checkAlias(path, r)))
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 4d1c0d29d4c1..a7e56e725a03 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
@@ -504,7 +504,7 @@ public void testBadPathDotDotPath() throws Exception
{
String response = connector.getResponse("GET /ooops/../../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 ");
- checkContains(response, 0, "reason: Bad URI");
+ checkContains(response, 0, "reason: Bad Request");
}
@Test
@@ -512,7 +512,7 @@ public void testBadDotDotPath() throws Exception
{
String response = connector.getResponse("GET ../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 ");
- checkContains(response, 0, "reason: Bad URI");
+ checkContains(response, 0, "reason: Bad Request");
}
@Test
@@ -520,7 +520,7 @@ public void testBadSlashDotDotPath() throws Exception
{
String response = connector.getResponse("GET /../path HTTP/1.0\r\nHost: localhost:80\r\n\n");
checkContains(response, 0, "HTTP/1.1 400 ");
- checkContains(response, 0, "reason: Bad URI");
+ checkContains(response, 0, "reason: Bad Request");
}
@Test
@@ -823,12 +823,6 @@ public void testBadUTF8FallsbackTo8859() throws Exception
LOG.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 200"); //now fallback to iso-8859-1
-
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/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
index e31f303438d7..7f2f91d7a64b 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
@@ -1713,15 +1713,6 @@ public void testAmbiguousPaths() throws Exception
assertThat(_connector.getResponse(request), Matchers.allOf(
startsWith("HTTP/1.1 200"),
containsString("pathInfo=/path/info")));
-
- _connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(
- UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR,
- UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT,
- UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER,
- UriCompliance.Violation.NON_CANONICAL_AMBIGUOUS_PATHS)));
- assertThat(_connector.getResponse(request), Matchers.allOf(
- startsWith("HTTP/1.1 200"),
- containsString("pathInfo=/path/ambiguous/.././info")));
}
@Test
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 9401376083bf..4f021b6c55f8 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
@@ -28,6 +28,8 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.nullValue;
@@ -218,24 +220,32 @@ public void testGetKnown() throws Exception
}
@Test
- public void testNormalize() throws Exception
+ public void testDoesNotExistResource() throws Exception
{
- final String path = "/down/.././index.html";
- Resource resource = context.getResource(path);
- assertEquals("index.html", resource.getFile().getName());
- assertEquals(docroot, resource.getFile().getParentFile());
- assertTrue(resource.exists());
-
- URL url = context.getServletContext().getResource(path);
- assertEquals(docroot, new File(url.toURI()).getParentFile());
+ Resource resource = context.getResource("/doesNotExist.html");
+ assertNotNull(resource);
+ assertFalse(resource.exists());
}
@Test
- public void testTooNormal() throws Exception
+ public void testAlias() throws Exception
{
- final String path = "/down/.././../";
- Resource resource = context.getResource(path);
+ Resource resource = context.getResource("/./index.html");
+ assertNotNull(resource);
+ assertFalse(resource.isAlias());
+
+ resource = context.getResource("/down/../index.html");
+ assertNotNull(resource);
+ assertFalse(resource.isAlias());
+
+ resource = context.getResource("//index.html");
assertNull(resource);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"/down/.././../", "/../down/"})
+ public void testNormalize(String path) throws Exception
+ {
URL url = context.getServletContext().getResource(path);
assertNull(url);
}
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
index 74702ec7cb8f..8315bcdab967 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
@@ -14,7 +14,6 @@
package org.eclipse.jetty.servlet;
import java.io.IOException;
-import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
@@ -420,8 +419,7 @@ else if (_servletContext instanceof ContextHandler.Context)
}
else
{
- URL u = _servletContext.getResource(pathInContext);
- r = _contextHandler.newResource(u);
+ return null;
}
if (LOG.isDebugEnabled())
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 730f29165fe3..3ee3295d5ea7 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
@@ -57,11 +57,13 @@ public static Stream data()
ret.add(Arguments.of("/hello?type=wo&rld", "/hello", "type=wo&rld"));
ret.add(Arguments.of("/hello?type=wo%20rld", "/hello", "type=wo%20rld"));
ret.add(Arguments.of("/hello?type=wo+rld", "/hello", "type=wo+rld"));
+ ret.add(Arguments.of("/hello?type=/a/../b/", "/hello", "type=/a/../b/"));
ret.add(Arguments.of("/It%27s%20me%21", "/It%27s%20me%21", null));
// try some slash encoding (with case preservation tests)
ret.add(Arguments.of("/hello%2fworld", "/hello%2fworld", null));
ret.add(Arguments.of("/hello%2Fworld", "/hello%2Fworld", null));
ret.add(Arguments.of("/%2f%2Fhello%2Fworld", "/%2f%2Fhello%2Fworld", null));
+
// try some "?" encoding (should not see as query string)
ret.add(Arguments.of("/hello%3Fworld", "/hello%3Fworld", null));
// try some strange encodings (should preserve them)
@@ -70,7 +72,7 @@ public static Stream data()
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)
- for (int i = 0x0; i < 0x1f; i++)
+ for (int i = 0x1; i < 0x1f; i++)
{
String raw = String.format("/hello%%%02Xworld", i);
ret.add(Arguments.of(raw, raw, null));
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java
index eca818907799..1492b1da1f24 100644
--- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java
@@ -34,7 +34,7 @@
import org.eclipse.jetty.util.ProcessorUtils;
/**
- * A servlet that uses the Servlet 3.1 asynchronous IO API to server
+ * A demonstration servlet that uses the Servlet 3.1 asynchronous IO API to server
* static content at a limited data rate.
*
* Two implementations are supported:
@@ -42,8 +42,7 @@
* APIs, but produces more garbage due to the byte[] nature of the API.
* - the
JettyDataStream
impl uses a Jetty API to write a ByteBuffer
* and thus allow the efficient use of file mapped buffers without any
- * temporary buffer copies (I did tell the JSR that this was a good idea to
- * have in the standard!).
+ * temporary buffer copies.
*
*
* The data rate is controlled by setting init parameters:
@@ -53,7 +52,9 @@
*
- pool
- The size of the thread pool used to service the writes (defaults to available processors)
*
* Thus if buffersize = 1024 and pause = 100, the data rate will be limited to 10KB per second.
+ * @deprecated this is intended as a demonstration and not production quality.
*/
+@Deprecated
public class DataRateLimitedServlet extends HttpServlet
{
private static final long serialVersionUID = -4771757707068097025L;
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PutFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PutFilter.java
index f4ca579d4df8..f34676387dc6 100644
--- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PutFilter.java
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PutFilter.java
@@ -57,6 +57,7 @@
* - putAtomic - boolean, if true PUT files are written to a temp location and moved into place.
*
*/
+@Deprecated
public class PutFilter implements Filter
{
public static final String __PUT = "PUT";
@@ -80,7 +81,8 @@ public void init(FilterConfig config) throws ServletException
_tmpdir = (File)_context.getAttribute("javax.servlet.context.tempdir");
- if (_context.getRealPath("/") == null)
+ String realPath = _context.getRealPath("/");
+ if (realPath == null)
throw new UnavailableException("Packed war");
String b = config.getInitParameter("baseURI");
@@ -90,7 +92,7 @@ public void init(FilterConfig config) throws ServletException
}
else
{
- File base = new File(_context.getRealPath("/"));
+ File base = new File(realPath);
_baseURI = base.toURI().toString();
}
@@ -284,7 +286,7 @@ public void handleDelete(HttpServletRequest request, HttpServletResponse respons
public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)
throws ServletException, IOException, URISyntaxException
{
- String newPath = URIUtil.canonicalEncodedPath(request.getHeader("new-uri"));
+ String newPath = URIUtil.canonicalURI(request.getHeader("new-uri"));
if (newPath == null)
{
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
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 f18e685f2e8b..b91b71c30081 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
@@ -471,6 +471,7 @@ public static String decodePath(String path, int offset, int length)
if (u == 'u')
{
// UTF16 encoding is only supported with UriCompliance.Violation.UTF16_ENCODINGS.
+ // This is wrong. This is a codepoint not a char
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5;
}
@@ -776,143 +777,128 @@ public static String parentPath(String p)
}
/**
- * Convert an encoded path to a canonical form.
+ * Convert a partial URI to a canonical form.
*
- * All instances of "." and ".." are factored out.
+ * All segments of "." and ".." are factored out.
* Null is returned if the path tries to .. above its root.
*
*
- * @param path the path to convert, decoded, with path separators '/' and no queries.
+ * @param uri the encoded URI from the path onwards, which may contain query strings and/or fragments
* @return the canonical path, or null if path traversal above root.
+ * @see #canonicalPath(String)
+ * @see #canonicalURI(String)
*/
- public static String canonicalPath(String path)
+ public static String canonicalURI(String uri)
{
- // See https://tools.ietf.org/html/rfc3986#section-5.2.4
-
- if (path == null || path.isEmpty())
- return path;
+ if (uri == null || uri.isEmpty())
+ return uri;
- int end = path.length();
+ boolean slash = true;
+ int end = uri.length();
int i = 0;
- int dots = 0;
+ // Initially just loop looking if we may need to normalize
loop: while (i < end)
{
- char c = path.charAt(i);
+ char c = uri.charAt(i);
switch (c)
{
case '/':
- dots = 0;
+ slash = true;
break;
case '.':
- if (dots == 0)
- {
- dots = 1;
+ if (slash)
break loop;
- }
- dots = -1;
+ slash = false;
break;
+ case '?':
+ case '#':
+ // Nothing to normalize so return original path
+ return uri;
+
default:
- dots = -1;
+ slash = false;
}
i++;
}
+ // Nothing to normalize so return original path
if (i == end)
- return path;
+ return uri;
- StringBuilder canonical = new StringBuilder(path.length());
- canonical.append(path, 0, i);
+ // We probably need to normalize, so copy to path so far into builder
+ StringBuilder canonical = new StringBuilder(uri.length());
+ canonical.append(uri, 0, i);
+ // Loop looking for single and double dot segments
+ int dots = 1;
i++;
- while (i <= end)
+ loop : while (i < end)
{
- char c = i < end ? path.charAt(i) : '\0';
+ char c = uri.charAt(i);
switch (c)
{
- case '\0':
- if (dots == 2)
- {
- if (canonical.length() < 2)
- return null;
- canonical.setLength(canonical.length() - 1);
- canonical.setLength(canonical.lastIndexOf("/") + 1);
- }
- break;
-
case '/':
- switch (dots)
- {
- case 1:
- break;
-
- case 2:
- if (canonical.length() < 2)
- return null;
- canonical.setLength(canonical.length() - 1);
- canonical.setLength(canonical.lastIndexOf("/") + 1);
- break;
-
- default:
- canonical.append(c);
- }
+ if (doDotsSlash(canonical, dots))
+ return null;
+ slash = true;
dots = 0;
break;
+ case '?':
+ case '#':
+ // finish normalization at a query
+ break loop;
+
case '.':
- switch (dots)
- {
- case 0:
- dots = 1;
- break;
- case 1:
- dots = 2;
- break;
- case 2:
- canonical.append("...");
- dots = -1;
- break;
- default:
- canonical.append('.');
- }
+ // Count dots only if they are leading in the segment
+ if (dots > 0)
+ dots++;
+ else if (slash)
+ dots = 1;
+ else
+ canonical.append('.');
+ slash = false;
break;
default:
- switch (dots)
- {
- case 1:
- canonical.append('.');
- break;
- case 2:
- canonical.append("..");
- break;
- default:
- }
+ // Add leading dots to the path
+ while (dots-- > 0)
+ canonical.append('.');
canonical.append(c);
- dots = -1;
+ dots = 0;
+ slash = false;
}
-
i++;
}
+
+ // process any remaining dots
+ if (doDots(canonical, dots))
+ return null;
+
+ // append any query
+ if (i < end)
+ canonical.append(uri, i, end);
+
return canonical.toString();
}
/**
- * Convert a path to a cananonical form.
- *
- * All instances of "." and ".." are factored out.
- *
+ * Convert a decoded URI path to a canonical form.
*
+ * All segments of "." and ".." are factored out.
* Null is returned if the path tries to .. above its root.
*
*
- * @param path the path to convert (expects URI/URL form, encoded, and with path separators '/')
+ * @param path the decoded URI path to convert. Any special characters (e.g. '?', "#") are assumed to be part of
+ * the path segments.
* @return the canonical path, or null if path traversal above root.
+ * @see #canonicalURI(String)
*/
- public static String canonicalEncodedPath(String path)
+ public static String canonicalPath(String path)
{
if (path == null || path.isEmpty())
return path;
@@ -921,8 +907,8 @@ public static String canonicalEncodedPath(String path)
int end = path.length();
int i = 0;
- loop:
- while (i < end)
+ // Initially just loop looking if we may need to normalize
+ loop: while (i < end)
{
char c = path.charAt(i);
switch (c)
@@ -937,9 +923,6 @@ public static String canonicalEncodedPath(String path)
slash = false;
break;
- case '?':
- return path;
-
default:
slash = false;
}
@@ -947,56 +930,31 @@ public static String canonicalEncodedPath(String path)
i++;
}
+ // Nothing to normalize so return original path
if (i == end)
return path;
+ // We probably need to normalize, so copy to path so far into builder
StringBuilder canonical = new StringBuilder(path.length());
canonical.append(path, 0, i);
+ // Loop looking for single and double dot segments
int dots = 1;
i++;
- while (i <= end)
+ while (i < end)
{
- char c = i < end ? path.charAt(i) : '\0';
+ char c = path.charAt(i);
switch (c)
{
- case '\0':
case '/':
- case '?':
- switch (dots)
- {
- case 0:
- if (c != '\0')
- canonical.append(c);
- break;
-
- case 1:
- if (c == '?')
- canonical.append(c);
- break;
-
- case 2:
- if (canonical.length() < 2)
- return null;
- canonical.setLength(canonical.length() - 1);
- canonical.setLength(canonical.lastIndexOf("/") + 1);
- if (c == '?')
- canonical.append(c);
- break;
- default:
- while (dots-- > 0)
- {
- canonical.append('.');
- }
- if (c != '\0')
- canonical.append(c);
- }
-
+ if (doDotsSlash(canonical, dots))
+ return null;
slash = true;
dots = 0;
break;
case '.':
+ // Count dots only if they are leading in the segment
if (dots > 0)
dots++;
else if (slash)
@@ -1007,20 +965,66 @@ else if (slash)
break;
default:
+ // Add leading dots to the path
while (dots-- > 0)
- {
canonical.append('.');
- }
canonical.append(c);
dots = 0;
slash = false;
}
-
i++;
}
+
+ // process any remaining dots
+ if (doDots(canonical, dots))
+ return null;
+
return canonical.toString();
}
+ private static boolean doDots(StringBuilder canonical, int dots)
+ {
+ switch (dots)
+ {
+ case 0:
+ case 1:
+ break;
+ case 2:
+ if (canonical.length() < 2)
+ return true;
+ canonical.setLength(canonical.length() - 1);
+ canonical.setLength(canonical.lastIndexOf("/") + 1);
+ break;
+ default:
+ while (dots-- > 0)
+ canonical.append('.');
+ }
+ return false;
+ }
+
+ private static boolean doDotsSlash(StringBuilder canonical, int dots)
+ {
+ switch (dots)
+ {
+ case 0:
+ canonical.append('/');
+ break;
+ case 1:
+ break;
+ case 2:
+ if (canonical.length() < 2)
+ return true;
+ canonical.setLength(canonical.length() - 1);
+ canonical.setLength(canonical.lastIndexOf("/") + 1);
+ break;
+ default:
+ while (dots-- > 0)
+ canonical.append('.');
+ canonical.append('/');
+ }
+ return false;
+ }
+
/**
* Convert a path to a compact form.
* All instances of "//" and "///" etc. are factored out to single "/"
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java
index f5e70fc7f352..ea7305ff86e2 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Utf8Appendable.java
@@ -213,7 +213,6 @@ public boolean append(byte[] b, int offset, int length, int maxChars)
protected void appendByte(byte b) throws IOException
{
-
if (b > 0 && _state == UTF8_ACCEPT)
{
_appendable.append((char)(b & 0xFF));
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 244ae2ad6338..755c4b5f3299 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
@@ -56,7 +56,7 @@ public class PathResource extends Resource
private final URI uri;
private final boolean belongsToDefaultFileSystem;
- private final Path checkAliasPath()
+ private Path checkAliasPath()
{
Path abs = path;
@@ -68,7 +68,6 @@ private final Path checkAliasPath()
* we will just use the original URI to construct the
* alias reference Path.
*/
-
if (!URIUtil.equalsIgnoreEncodings(uri, path.toUri()))
{
try
@@ -85,9 +84,11 @@ private final Path checkAliasPath()
}
if (!abs.isAbsolute())
- {
abs = path.toAbsolutePath();
- }
+
+ Path normal = path.normalize();
+ if (!abs.equals(normal))
+ return normal;
try
{
@@ -233,8 +234,7 @@ public PathResource(Path path)
LOG.debug("Unable to get real/canonical path for {}", path, e);
}
- // cleanup any lingering relative path nonsense (like "/./" and "/../")
- this.path = absPath.normalize();
+ this.path = absPath;
assertValidPath(path);
this.uri = this.path.toUri();
@@ -254,7 +254,7 @@ private PathResource(PathResource parent, String childPath)
// Calculate the URI and the path separately, so that any aliasing done by
// FileSystem.getPath(path,childPath) is visible as a difference to the URI
// obtained via URIUtil.addDecodedPath(uri,childPath)
-
+ // The checkAliasPath normalization checks will only work correctly if the getPath implementation here does not normalize.
this.path = parent.path.getFileSystem().getPath(parent.path.toString(), childPath);
if (isDirectory() && !childPath.endsWith("/"))
childPath += "/";
@@ -350,12 +350,10 @@ public boolean isSame(Resource resource)
@Override
public Resource addPath(final String subpath) throws IOException
{
- String cpath = URIUtil.canonicalPath(subpath);
-
- if ((cpath == null) || (cpath.length() == 0))
+ if ((subpath == null) || (subpath.length() == 0))
throw new MalformedURLException(subpath);
- if ("/".equals(cpath))
+ if ("/".equals(subpath))
return this;
// subpaths are always under PathResource
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 a45421a2a2ac..03afc1fd2efc 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
@@ -477,7 +477,6 @@ public URI getAlias()
*/
public String getListHTML(String base, boolean parent, String query) throws IOException
{
- 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 74a260c2ecda..b15d3e26f07b 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
@@ -280,9 +280,8 @@ public String[] list()
public Resource addPath(String path)
throws IOException
{
- Objects.requireNonNull(path, "Path may not be null");
-
- path = URIUtil.canonicalPath(path);
+ if (path == null)
+ throw new MalformedURLException("null 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 6a9c88d494ea..d8bac6a9ea5f 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
@@ -22,10 +22,11 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
public class URIUtilCanonicalPathTest
{
- public static Stream data()
+ public static Stream paths()
{
String[][] canonical =
{
@@ -83,6 +84,7 @@ public static Stream data()
{"/foo/../bar//", "/bar//"},
{"/ctx/../bar/../ctx/all/index.txt", "/ctx/all/index.txt"},
{"/down/.././index.html", "/index.html"},
+ {"/aaa/bbb/ccc/..", "/aaa/bbb/"},
// Path traversal up past root
{"..", null},
@@ -95,10 +97,8 @@ public static Stream data()
{"a/../..", null},
{"/foo/../../bar", null},
- // Query parameter specifics
- {"/ctx/dir?/../index.html", "/ctx/index.html"},
- {"/get-files?file=/etc/passwd", "/get-files?file=/etc/passwd"},
- {"/get-files?file=../../../../../passwd", null},
+ // Encoded ?
+ {"/ctx/dir%3f/../index.html", "/ctx/index.html"},
// Known windows shell quirks
{"file.txt ", "file.txt "}, // with spaces
@@ -120,7 +120,6 @@ public static Stream data()
{"/%2e%2e/", "/%2e%2e/"},
// paths with parameters are not elided
- // canonicalPath() is not responsible for decoding characters
{"/foo/.;/bar", "/foo/.;/bar"},
{"/foo/..;/bar", "/foo/..;/bar"},
{"/foo/..;/..;/bar", "/foo/..;/..;/bar"},
@@ -139,9 +138,23 @@ public static Stream data()
}
@ParameterizedTest
- @MethodSource("data")
+ @MethodSource("paths")
public void testCanonicalPath(String input, String expectedResult)
{
+ // Check canonicalPath
assertThat(URIUtil.canonicalPath(input), is(expectedResult));
+
+ // Check canonicalURI
+ if (expectedResult == null)
+ assertThat(URIUtil.canonicalURI(input), nullValue());
+ else
+ {
+ // mostly encodedURI will be the same
+ assertThat(URIUtil.canonicalURI(input), is(expectedResult));
+ // but will terminate on fragments and queries
+ assertThat(URIUtil.canonicalURI(input + "?/foo/../bar/."), is(expectedResult + "?/foo/../bar/."));
+ assertThat(URIUtil.canonicalURI(input + "#/foo/../bar/."), is(expectedResult + "#/foo/../bar/."));
+ }
}
+
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java
index 384a2aaf6458..4786da17eb01 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/Utf8AppendableTest.java
@@ -151,6 +151,35 @@ public void testInvalidUTF8(Class impl) throws UnsupportedEncodi
});
}
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testInvalidZeroUTF8(Class impl) throws UnsupportedEncodingException
+ {
+ // From https://datatracker.ietf.org/doc/html/rfc3629#section-10
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ buffer.append((byte)0xC0);
+ buffer.append((byte)0x80);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("implementations")
+ public void testInvalidAlternateDotEncodingUTF8(Class impl) throws UnsupportedEncodingException
+ {
+ // From https://datatracker.ietf.org/doc/html/rfc3629#section-10
+ assertThrows(Utf8Appendable.NotUtf8Exception.class, () ->
+ {
+ Utf8Appendable buffer = impl.getDeclaredConstructor().newInstance();
+ buffer.append((byte)0x2f);
+ buffer.append((byte)0xc0);
+ buffer.append((byte)0xae);
+ buffer.append((byte)0x2e);
+ buffer.append((byte)0x2f);
+ });
+ }
+
@ParameterizedTest
@MethodSource("implementations")
public void testFastFail1(Class impl) throws Exception
diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
index ecd65c66fcc6..1a2af27da4c7 100644
--- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
+++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
@@ -400,7 +400,7 @@ public Resource getResource(String uriInContext) throws MalformedURLException
if (uriInContext == null || !uriInContext.startsWith(URIUtil.SLASH))
throw new MalformedURLException(uriInContext);
- IOException ioe = null;
+ MalformedURLException mue = null;
Resource resource = null;
int loop = 0;
while (uriInContext != null && loop++ < 100)
@@ -413,16 +413,16 @@ public Resource getResource(String uriInContext) throws MalformedURLException
uriInContext = getResourceAlias(uriInContext);
}
- catch (IOException e)
+ catch (MalformedURLException e)
{
LOG.trace("IGNORED", e);
- if (ioe == null)
- ioe = e;
+ if (mue == null)
+ mue = e;
}
}
- if (ioe instanceof MalformedURLException)
- throw (MalformedURLException)ioe;
+ if (mue != null)
+ throw mue;
return resource;
}
@@ -1438,6 +1438,9 @@ public void checkListener(Class extends EventListener> listener) throws Illega
@Override
public URL getResource(String path) throws MalformedURLException
{
+ if (path == null)
+ return null;
+
Resource resource = WebAppContext.this.getResource(path);
if (resource == null || !resource.exists())
return null;
diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java
index 83f67fdc2d2d..765a21867813 100644
--- a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java
+++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebAppContextTest.java
@@ -53,6 +53,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -328,6 +329,45 @@ public void testProtectedTarget() throws Exception
assertThat(HttpTester.parseResponse(connector.getResponse("GET //WEB-INF/test.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
assertThat(HttpTester.parseResponse(connector.getResponse("GET /WEB-INF%2ftest.xml HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(), is(HttpStatus.NOT_FOUND_404));
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "/WEB-INF",
+ "/WEB-INF/",
+ "/WEB-INF/test.xml",
+ "/web-inf/test.xml",
+ "/%2e/WEB-INF/test.xml",
+ "/%2e/%2e/WEB-INF/test.xml",
+ "/foo/%2e%2e/WEB-INF/test.xml",
+ "/%2E/WEB-INF/test.xml",
+ "//WEB-INF/test.xml",
+ "/WEB-INF%2ftest.xml",
+ "/.%00/WEB-INF/test.xml",
+ "/WEB-INF%00/test.xml"
+ })
+ public void testProtectedTargetFailure(String path) throws Exception
+ {
+ Server server = newServer();
+
+ LocalConnector connector = new LocalConnector(server);
+ server.addConnector(connector);
+ connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
+
+ HandlerList handlers = new HandlerList();
+ ContextHandlerCollection contexts = new ContextHandlerCollection();
+ WebAppContext context = new WebAppContext();
+ Path testWebapp = MavenTestingUtils.getProjectDirPath("src/test/webapp");
+ context.setBaseResource(new PathResource(testWebapp));
+ context.setContextPath("/");
+ server.setHandler(handlers);
+ handlers.addHandler(contexts);
+ contexts.addHandler(context);
+
+ server.start();
+
+ assertThat(HttpTester.parseResponse(connector.getResponse("GET " + path + " HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n")).getStatus(),
+ Matchers.anyOf(is(HttpStatus.NOT_FOUND_404), is(HttpStatus.BAD_REQUEST_400)));
+ }
@Test
public void testNullPath() throws Exception
diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java
index 3cbf66f8ac37..f841c023ecc4 100644
--- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java
+++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/jsp/JspAndDefaultWithoutAliasesTest.java
@@ -22,8 +22,12 @@
import java.util.List;
import java.util.stream.Stream;
+import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.security.HashLoginService;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
@@ -56,13 +60,21 @@ public static Stream aliases()
data.add(Arguments.of("/dump.jsp"));
data.add(Arguments.of("/dump.jsp/"));
- data.add(Arguments.of("/dump.jsp%00"));
- data.add(Arguments.of("/dump.jsp%00x"));
- data.add(Arguments.of("/dump.jsp%00x/dump.jsp"));
- data.add(Arguments.of("/dump.jsp%00/dump.jsp"));
- data.add(Arguments.of("/dump.jsp%00/index.html"));
- data.add(Arguments.of("/dump.jsp%00/"));
- data.add(Arguments.of("/dump.jsp%00x/"));
+ data.add(Arguments.of("/dump.jsp%1e"));
+ data.add(Arguments.of("/dump.jsp%1ex"));
+ data.add(Arguments.of("/dump.jsp%1ex/dump.jsp"));
+ data.add(Arguments.of("/dump.jsp%1e/dump.jsp"));
+ data.add(Arguments.of("/dump.jsp%1e/index.html"));
+ data.add(Arguments.of("/dump.jsp%1e/"));
+ data.add(Arguments.of("/dump.jsp%1ex/"));
+ // The _00_ is later replaced with a real null character in a customizer
+ data.add(Arguments.of("/dump.jsp_00_"));
+ data.add(Arguments.of("/dump.jsp_00_"));
+ data.add(Arguments.of("/dump.jsp_00_/dump.jsp"));
+ data.add(Arguments.of("/dump.jsp_00_/dump.jsp"));
+ data.add(Arguments.of("/dump.jsp_00_/index.html"));
+ data.add(Arguments.of("/dump.jsp_00_/"));
+ data.add(Arguments.of("/dump.jsp_00_/"));
return data.stream();
}
@@ -98,6 +110,17 @@ public static void startServer() throws Exception
// add context
server.setHandler(context);
+ // Add customizer to convert "_00_" to a real null
+ server.getContainedBeans(HttpConfiguration.class).forEach(config ->
+ {
+ config.addCustomizer((connector, channelConfig, request) ->
+ {
+ HttpURI uri = request.getHttpURI();
+ if (uri.getPath().contains("_00_"))
+ request.setHttpURI(HttpURI.build(uri, uri.getPath().replace("_00_", "\000"), uri.getParam(), uri.getQuery()));
+ });
+ });
+
server.start();
int port = ((NetworkConnector)server.getConnectors()[0]).getLocalPort();
From 4cf9ca70c26512d62815cf979a81d1c55c45b74f Mon Sep 17 00:00:00 2001
From: Simone Bordet
Date: Tue, 29 Jun 2021 00:07:56 +0200
Subject: [PATCH 2/6] 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 | 62 ++++++++-----------
.../org/eclipse/jetty/http/HttpURITest.java | 21 +++++++
.../maven/plugin/MavenWebAppContext.java | 1 +
.../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/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 ++++++
16 files changed, 197 insertions(+), 70 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 596f1acc1424..e9db3f2fc5d7 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
@@ -34,7 +34,7 @@
* via the static methods such as {@link #build()} and {@link #from(String)}.
*
* A URI such as
- * 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
@@ -62,11 +62,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()}.
@@ -80,32 +82,32 @@ public interface HttpURI
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");
@@ -217,12 +219,12 @@ static Immutable build(HttpURI schemeHostPort, HttpURI uri)
boolean isAbsolute();
/**
- * @return True if the URI has either an {@link #hasAmbiguousParameter()}, {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousSeparator()}.
+ * @return True if the URI has any ambiguous {@link Violation}s.
*/
boolean isAmbiguous();
/**
- * @return True if the URI has any Violations.
+ * @return True if the URI has any {@link Violation}s.
*/
boolean hasViolations();
@@ -1319,6 +1321,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);
@@ -1405,26 +1409,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;
}
@@ -1439,7 +1428,9 @@ else if (c == '/')
break;
}
default:
+ {
throw new IllegalStateException(state.toString());
+ }
}
}
@@ -1493,8 +1484,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");
}
@@ -1515,7 +1506,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);
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 45e20953fa10..0d56bba97fbc 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
@@ -36,6 +36,7 @@
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
+// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
public class HttpURITest
{
@Test
@@ -358,6 +359,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)},
@@ -369,6 +371,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)},
@@ -803,4 +808,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 = HttpURI.build(input);
+ assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
+ }
}
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebAppContext.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebAppContext.java
index c246f9688511..74a60614ba0a 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebAppContext.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebAppContext.java
@@ -361,6 +361,7 @@ public Resource getResource(String uriInContext) throws MalformedURLException
// /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;
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 058dac8293da..750c4ad2ea4c 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
@@ -48,12 +48,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 363aedb3ad0c..17f370e60948 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
@@ -20,6 +20,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")
@@ -71,6 +72,12 @@ public void testInvalidUrlWithMessage() throws Exception
@Test
public void testInvalidJsp() throws Exception
+ {
+ assertThrows(IllegalArgumentException.class, () -> HttpURI.build(_request.getHttpURI(), "/jsp/bean1.jsp%00"));
+ }
+
+ @Test
+ public void testInvalidJspWithNullByte() throws Exception
{
_rule.setCode("405");
_rule.setMessage("foo");
@@ -83,6 +90,12 @@ public void testInvalidJsp() throws Exception
assertEquals("foo", _request.getAttribute(Dispatcher.ERROR_MESSAGE));
}
+ @Test
+ public void testInvalidShamrock() throws Exception
+ {
+ assertThrows(IllegalArgumentException.class, () -> HttpURI.build(_request.getHttpURI(), "/jsp/shamrock-%00%E2%98%98.jsp"));
+ }
+
@Test
public void testValidShamrock() throws Exception
{
@@ -106,4 +119,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 6fe16df2bfb6..f496fa782c63 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
@@ -1926,13 +1926,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))
@@ -2274,6 +2272,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 5a571fada0f2..bd7ba44c5297 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
@@ -168,8 +168,6 @@ public Resource getResource(String path) throws IOException
else if (_context != null)
{
r = _context.getResource(path);
- if (r != null)
- return r;
}
if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
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 a7e56e725a03..b6d301285445 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
@@ -823,6 +823,12 @@ public void testBadUTF8FallsbackTo8859() throws Exception
LOG.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 4f021b6c55f8..d304c0bb2160 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
@@ -230,16 +230,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 3ee3295d5ea7..13228219a49b 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
@@ -71,7 +71,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);
@@ -196,7 +197,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));
@@ -223,4 +223,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 b91b71c30081..d706b230b010 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
@@ -533,7 +533,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/PathResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java
index 755c4b5f3299..69c438e4adab 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
@@ -86,8 +86,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
@@ -97,11 +100,8 @@ private Path checkAliasPath()
if (Files.exists(path))
{
Path real = abs.toRealPath(FOLLOW_LINKS);
-
if (!isSameName(abs, real))
- {
return real;
- }
}
}
catch (IOException e)
@@ -348,20 +348,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 03afc1fd2efc..585855dd0cb2 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
@@ -419,10 +419,12 @@ public abstract boolean renameTo(Resource dest)
* Returns the resource contained inside the current resource with the
* given name, which may or may not exist.
*
- * @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, never null
* @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;
@@ -477,6 +479,8 @@ public URI getAlias()
*/
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 b15d3e26f07b..6ed655080333 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
@@ -280,8 +280,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 d8bac6a9ea5f..d6e11da38ce1 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
@@ -146,7 +146,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
@@ -157,4 +159,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 1ee036f58548..b1787fe2d726 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
@@ -16,6 +16,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;
@@ -40,6 +41,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
{
@@ -320,4 +323,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"));
+ }
}
From 7b2d94b286fda992b7dcca500ac7618552a84ea5 Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Tue, 29 Jun 2021 14:09:51 +1000
Subject: [PATCH 3/6] Fix failing tests.
Signed-off-by: Lachlan Roberts
---
.../java/org/eclipse/jetty/http/HttpURI.java | 22 -------------------
.../org/eclipse/jetty/server/Dispatcher.java | 8 ++++---
.../eclipse/jetty/servlet/RequestURITest.java | 6 +++--
.../java/org/eclipse/jetty/util/URIUtil.java | 12 +++++++++-
4 files changed, 20 insertions(+), 28 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 e9db3f2fc5d7..f1611992be1c 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
@@ -18,7 +18,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
-import java.util.Set;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index;
@@ -178,11 +177,6 @@ static Immutable from(String scheme, String host, int port, String pathQuery)
return new Mutable(scheme, host, port, pathQuery).asImmutable();
}
- static Immutable build(HttpURI schemeHostPort, HttpURI uri)
- {
- return new Immutable(schemeHostPort, uri);
- }
-
Immutable asImmutable();
String asString();
@@ -322,22 +316,6 @@ private Immutable(String uri)
_decodedPath = null;
}
- private Immutable(HttpURI schemeHostPort, HttpURI uri)
- {
- _scheme = schemeHostPort.getScheme();
- _user = schemeHostPort.getUser();
- _host = schemeHostPort.getHost();
- _port = schemeHostPort.getPort();
- _path = uri.getPath();
- _param = uri.getParam();
- _query = uri.getQuery();
- _fragment = uri.getFragment();
- _uri = null;
- _decodedPath = uri.getDecodedPath();
- if (uri.hasViolations())
- _violations.addAll(uri.getViolations());
- }
-
@Override
public Immutable asImmutable()
{
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
index 44a8dfc7eb43..e8a917424864 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java
@@ -27,10 +27,8 @@
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException;
-import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.UriCompliance;
-import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.MultiMap;
@@ -185,7 +183,11 @@ protected void forward(ServletRequest request, ServletResponse response, Dispatc
source_mapping,
old_uri.getQuery()));
- baseRequest.setHttpURI(HttpURI.build(old_uri, _uri));
+ String query = _uri.getQuery();
+ if (query == null)
+ query = old_uri.getQuery();
+
+ baseRequest.setHttpURI(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query));
baseRequest.setContext(_contextHandler.getServletContext(), _pathInContext);
baseRequest.setServletPathMapping(null);
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 13228219a49b..8c08fe06be09 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
@@ -71,6 +71,10 @@ 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"));
+
+ ret.add(Arguments.of("/hello/..;/world", "/hello/..;/world", null));
+ ret.add(Arguments.of("/hello/..;?/world", "/hello/..;", "/world"));
+
// 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++)
@@ -236,8 +240,6 @@ public static Stream badData()
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"));
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 d706b230b010..1a8b65aad919 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
@@ -785,7 +785,6 @@ public static String parentPath(String p)
* @param uri the encoded URI from the path onwards, which may contain query strings and/or fragments
* @return the canonical path, or null if path traversal above root.
* @see #canonicalPath(String)
- * @see #canonicalURI(String)
*/
public static String canonicalURI(String uri)
{
@@ -885,6 +884,17 @@ else if (slash)
return canonical.toString();
}
+ /**
+ * @param path the encoded URI from the path onwards, which may contain query strings and/or fragments
+ * @return the canonical path, or null if path traversal above root.
+ * @deprecated Use {@link #canonicalURI(String)}
+ */
+ @Deprecated
+ public static String canonicalEncodedPath(String path)
+ {
+ return canonicalURI(path);
+ }
+
/**
* Convert a decoded URI path to a canonical form.
*
From 37c657ae1259cdfe946fc348f0f8951eabbc44cb Mon Sep 17 00:00:00 2001
From: Lachlan Roberts
Date: Tue, 29 Jun 2021 15:59:34 +1000
Subject: [PATCH 4/6] fix javadoc grammar
Signed-off-by: Lachlan Roberts
---
jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 f1611992be1c..9f9aab0ac1e7 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
@@ -34,7 +34,7 @@
*
* A URI such as
* {@code http://user@host:port/path;param1/%2e/info;param2?query#fragment}
- * this class will split it into the following optional elements:
+ * are split into the following optional elements:
* - {@link #getScheme()} - http:
* - {@link #getAuthority()} - //name@host:port
* - {@link #getHost()} - host
From 66b1cbb60e539ce8ff22a78fdffcf04cddf32c4a Mon Sep 17 00:00:00 2001
From: Lachlan
Date: Tue, 29 Jun 2021 17:23:53 +1000
Subject: [PATCH 5/6] Update
jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
Co-authored-by: Greg Wilkins
---
jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 9f9aab0ac1e7..84e13576db18 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
@@ -34,7 +34,7 @@
*
* A URI such as
* {@code http://user@host:port/path;param1/%2e/info;param2?query#fragment}
- * are split into the following optional elements:
+ * is split into the following optional elements:
* - {@link #getScheme()} - http:
* - {@link #getAuthority()} - //name@host:port
* - {@link #getHost()} - host
From 9d98fe7a4d610d3dacaedd2f0971be6a6590eb9f Mon Sep 17 00:00:00 2001
From: Greg Wilkins
Date: Tue, 29 Jun 2021 21:32:01 +1000
Subject: [PATCH 6/6] Compliance mode HttpURI uses UriCompliance.Violation
Signed-off-by: Greg Wilkins
---
.../java/org/eclipse/jetty/http/HttpURI.java | 184 ++++--------------
.../org/eclipse/jetty/http/UriCompliance.java | 41 +---
.../org/eclipse/jetty/http/HttpURITest.java | 178 ++++++++---------
.../jetty/server/handler/ContextHandler.java | 9 +
4 files changed, 144 insertions(+), 268 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 84e13576db18..1294874db368 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
@@ -19,6 +19,7 @@
import java.util.Collections;
import java.util.EnumSet;
+import org.eclipse.jetty.http.UriCompliance.Violation;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil;
@@ -75,54 +76,6 @@
**/
public interface HttpURI
{
- /**
- * Violations of safe URI interpretations
- */
- enum Violation
- {
- /**
- * Ambiguous path segments e.g. {@code /foo/%2E%2E/bar}
- */
- SEGMENT("Ambiguous path segments"),
-
- /**
- * 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. {@code /foo/..;/bar}
- */
- PARAM("Ambiguous path parameters"),
-
- /**
- * Ambiguous double encoding within a URI segment e.g. {@code /%2557EB-INF}
- */
- ENCODING("Ambiguous double encoding"),
-
- /**
- * Ambiguous empty segments e.g. {@code /foo//bar}
- */
- EMPTY("Ambiguous empty segments"),
-
- /**
- * Non standard UTF-16 encoding eg {@code /foo%u2192bar}.
- */
- UTF16("Non standard UTF-16 encoding");
-
- private final String _message;
-
- Violation(String message)
- {
- _message = message;
- }
-
- String getMessage()
- {
- return _message;
- }
- }
-
static Mutable build()
{
return new Mutable();
@@ -236,29 +189,50 @@ static Immutable from(String scheme, String host, int port, String pathQuery)
/**
* @return True if the URI has a possibly ambiguous segment like '..;' or '%2e%2e'
*/
- boolean hasAmbiguousSegment();
+ default boolean hasAmbiguousSegment()
+ {
+ return hasViolation(Violation.AMBIGUOUS_PATH_SEGMENT);
+ }
/**
* @return True if the URI empty segment that is ambiguous like '//' or '/;param/'.
*/
- boolean hasAmbiguousEmptySegment();
+ default boolean hasAmbiguousEmptySegment()
+ {
+ return hasViolation(Violation.AMBIGUOUS_EMPTY_SEGMENT);
+ }
/**
* @return True if the URI has a possibly ambiguous separator of %2f
*/
- boolean hasAmbiguousSeparator();
+ default boolean hasAmbiguousSeparator()
+ {
+ return hasViolation(Violation.AMBIGUOUS_PATH_SEPARATOR);
+ }
/**
* @return True if the URI has a possibly ambiguous path parameter like '..;'
*/
- boolean hasAmbiguousParameter();
+ default boolean hasAmbiguousParameter()
+ {
+ return hasViolation(Violation.AMBIGUOUS_PATH_PARAMETER);
+ }
/**
* @return True if the URI has an encoded '%' character.
*/
- boolean hasAmbiguousEncoding();
+ default boolean hasAmbiguousEncoding()
+ {
+ return hasViolation(Violation.AMBIGUOUS_PATH_ENCODING);
+ }
- boolean hasUtf16Encoding();
+ /**
+ * @return True if the URI has UTF16 '%u' encodings.
+ */
+ default boolean hasUtf16Encoding()
+ {
+ return hasViolation(Violation.UTF16_ENCODINGS);
+ }
default URI toURI()
{
@@ -466,7 +440,7 @@ public boolean isAbsolute()
@Override
public boolean isAmbiguous()
{
- return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16));
+ return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16_ENCODINGS));
}
@Override
@@ -484,43 +458,7 @@ public boolean hasViolation(Violation violation)
@Override
public Collection getViolations()
{
- return Collections.unmodifiableSet(_violations);
- }
-
- @Override
- public boolean hasAmbiguousSegment()
- {
- return _violations.contains(Violation.SEGMENT);
- }
-
- @Override
- public boolean hasAmbiguousEmptySegment()
- {
- return _violations.contains(Violation.EMPTY);
- }
-
- @Override
- public boolean hasAmbiguousSeparator()
- {
- return _violations.contains(Violation.SEPARATOR);
- }
-
- @Override
- public boolean hasAmbiguousParameter()
- {
- return _violations.contains(Violation.PARAM);
- }
-
- @Override
- public boolean hasAmbiguousEncoding()
- {
- return _violations.contains(Violation.ENCODING);
- }
-
- @Override
- public boolean hasUtf16Encoding()
- {
- return _violations.contains(Violation.UTF16);
+ return Collections.unmodifiableCollection(_violations);
}
@Override
@@ -851,7 +789,7 @@ public boolean isAbsolute()
@Override
public boolean isAmbiguous()
{
- return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16));
+ return !_violations.isEmpty() && !(_violations.size() == 1 && _violations.contains(Violation.UTF16_ENCODINGS));
}
@Override
@@ -869,43 +807,7 @@ public boolean hasViolation(Violation violation)
@Override
public Collection getViolations()
{
- return Collections.unmodifiableSet(_violations);
- }
-
- @Override
- public boolean hasAmbiguousSegment()
- {
- return _violations.contains(Violation.SEGMENT);
- }
-
- @Override
- public boolean hasAmbiguousEmptySegment()
- {
- return _violations.contains(Violation.EMPTY);
- }
-
- @Override
- public boolean hasAmbiguousSeparator()
- {
- return _violations.contains(Violation.SEPARATOR);
- }
-
- @Override
- public boolean hasAmbiguousParameter()
- {
- return _violations.contains(Violation.PARAM);
- }
-
- @Override
- public boolean hasAmbiguousEncoding()
- {
- return _violations.contains(Violation.ENCODING);
- }
-
- @Override
- public boolean hasUtf16Encoding()
- {
- return _violations.contains(Violation.UTF16);
+ return Collections.unmodifiableCollection(_violations);
}
public Mutable normalize()
@@ -1010,9 +912,9 @@ public Mutable uri(HttpURI uri)
_uri = null;
_decodedPath = uri.getDecodedPath();
if (uri.hasAmbiguousSeparator())
- _violations.add(Violation.SEPARATOR);
+ _violations.add(Violation.AMBIGUOUS_PATH_SEPARATOR);
if (uri.hasAmbiguousSegment())
- _violations.add(Violation.SEGMENT);
+ _violations.add(Violation.AMBIGUOUS_PATH_SEGMENT);
return this;
}
@@ -1287,7 +1189,7 @@ else if (c == '/')
{
if (encodedCharacters == 2 && c == 'u' && !encodedUtf16)
{
- _violations.add(Violation.UTF16);
+ _violations.add(Violation.UTF16_ENCODINGS);
encodedUtf16 = true;
encodedCharacters = 4;
continue;
@@ -1299,14 +1201,14 @@ 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.
+ // 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);
+ _violations.add(Violation.AMBIGUOUS_PATH_SEPARATOR);
break;
case '%':
- _violations.add(Violation.ENCODING);
+ _violations.add(Violation.AMBIGUOUS_PATH_ENCODING);
break;
default:
break;
@@ -1487,7 +1389,7 @@ private void checkSegment(String uri, int segment, int end, boolean param)
// 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);
+ _violations.add(Violation.AMBIGUOUS_EMPTY_SEGMENT);
if (end == segment)
{
@@ -1498,7 +1400,7 @@ private void checkSegment(String uri, int segment, int end, boolean param)
// If this empty segment is the first segment then it is ambiguous.
if (segment == 0)
{
- _violations.add(Violation.EMPTY);
+ _violations.add(Violation.AMBIGUOUS_EMPTY_SEGMENT);
return;
}
@@ -1515,12 +1417,12 @@ private void checkSegment(String uri, int segment, int end, boolean param)
if (ambiguous == Boolean.TRUE)
{
// The segment is always ambiguous.
- _violations.add(Violation.SEGMENT);
+ _violations.add(Violation.AMBIGUOUS_PATH_SEGMENT);
}
else if (param && ambiguous == Boolean.FALSE)
{
// The segment is ambiguous only when followed by a parameter.
- _violations.add(Violation.PARAM);
+ _violations.add(Violation.AMBIGUOUS_PATH_PARAMETER);
}
}
}
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 b82b13c7e5a5..be8ceffdc77e 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
@@ -14,7 +14,6 @@
package org.eclipse.jetty.http;
import java.util.Arrays;
-import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
@@ -26,7 +25,6 @@
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static java.util.EnumSet.allOf;
-import static java.util.EnumSet.complementOf;
import static java.util.EnumSet.noneOf;
import static java.util.EnumSet.of;
@@ -332,45 +330,12 @@ private static Set copyOf(Set violations)
return EnumSet.copyOf(violations);
}
- private static final EnumMap __uriViolations = new EnumMap<>(HttpURI.Violation.class);
-
- static
- {
- // create a map from Violation to compliance in a loop, so that any new violations added are detected with ISE
- for (HttpURI.Violation violation : HttpURI.Violation.values())
- {
- switch (violation)
- {
- case SEPARATOR:
- __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_SEPARATOR);
- break;
- case SEGMENT:
- __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_SEGMENT);
- break;
- case PARAM:
- __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_PARAMETER);
- break;
- case ENCODING:
- __uriViolations.put(violation, Violation.AMBIGUOUS_PATH_ENCODING);
- break;
- case EMPTY:
- __uriViolations.put(violation, Violation.AMBIGUOUS_EMPTY_SEGMENT);
- break;
- case UTF16:
- __uriViolations.put(violation, Violation.UTF16_ENCODINGS);
- break;
- default:
- throw new IllegalStateException();
- }
- }
- }
-
public static String checkUriCompliance(UriCompliance compliance, HttpURI uri)
{
- for (HttpURI.Violation violation : HttpURI.Violation.values())
+ for (UriCompliance.Violation violation : UriCompliance.Violation.values())
{
- if (uri.hasViolation(violation) && (compliance == null || !compliance.allows(__uriViolations.get(violation))))
- return violation.getMessage();
+ if (uri.hasViolation(violation) && (compliance == null || !compliance.allows(violation)))
+ return violation.getDescription();
}
return null;
}
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 0d56bba97fbc..83701d915624 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
@@ -19,7 +19,7 @@
import java.util.EnumSet;
import java.util.stream.Stream;
-import org.eclipse.jetty.http.HttpURI.Violation;
+import org.eclipse.jetty.http.UriCompliance.Violation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -357,9 +357,9 @@ public static Stream decodePathTests()
// encoded paths
{"/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)},
+ {"/f%u006f%u006F/bar", "/foo/bar", EnumSet.of(Violation.UTF16_ENCODINGS)},
+ {"/f%u0001%u0001/bar", "/f\001\001/bar", EnumSet.of(Violation.UTF16_ENCODINGS)},
+ {"/foo/%u20AC/bar", "/foo/\u20AC/bar", EnumSet.of(Violation.UTF16_ENCODINGS)},
// illegal paths
{"//host/../path/info", null, EnumSet.noneOf(Violation.class)},
@@ -390,60 +390,60 @@ public static Stream decodePathTests()
{"..;param/info", null, EnumSet.noneOf(Violation.class)},
// ambiguous dot encodings
- {"scheme://host/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
- {"scheme:/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
- {"path/%2e/info/", "path/info/", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.SEGMENT)},
- {"%2e/info", "info", EnumSet.of(Violation.SEGMENT)},
- {"%u002e/info", "info", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
-
- {"%2e", "", EnumSet.of(Violation.SEGMENT)},
- {"%u002e", "", EnumSet.of(Violation.SEGMENT, Violation.UTF16)},
+ {"scheme://host/path/%2e/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"scheme:/path/%2e/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"path/%2e/info/", "path/info/", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"%2e/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"%u002e/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.UTF16_ENCODINGS)},
+
+ {"%2e", "", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"%u002e", "", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.UTF16_ENCODINGS)},
// empty segment treated as ambiguous
- {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
- {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.EMPTY)},
- {"/foo///../../../bar", "/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo//bar", "/foo//bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo///../../../bar", "/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"/foo/./../bar", "/bar", EnumSet.noneOf(Violation.class)},
- {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"foo/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
{"foo;/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
- {";/bar", "/bar", EnumSet.of(Violation.EMPTY)},
- {";?n=v", "", EnumSet.of(Violation.EMPTY)},
+ {";/bar", "/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {";?n=v", "", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"?n=v", "", EnumSet.noneOf(Violation.class)},
{"#n=v", "", EnumSet.noneOf(Violation.class)},
{"", "", EnumSet.noneOf(Violation.class)},
{"http:/foo", "/foo", EnumSet.noneOf(Violation.class)},
// ambiguous parameter inclusions
- {"/path/.;/info", "/path/info", EnumSet.of(Violation.PARAM)},
- {"/path/.;param/info", "/path/info", EnumSet.of(Violation.PARAM)},
- {"/path/..;/info", "/info", EnumSet.of(Violation.PARAM)},
- {"/path/..;param/info", "/info", EnumSet.of(Violation.PARAM)},
- {".;/info", "info", EnumSet.of(Violation.PARAM)},
- {".;param/info", "info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/.;param/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/..;/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/..;param/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {".;/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {".;param/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
// ambiguous segment separators
- {"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
- {"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
- {"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
- {"/path/%2f../info", "/path/info", EnumSet.of(Violation.SEPARATOR)},
+ {"/path/%2f/info", "/path///info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
+ {"%2f/info", "//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
+ {"%2F/info", "//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
+ {"/path/%2f../info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
// ambiguous encoding
- {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
- {"/path/%u0025/info", "/path/%/info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
- {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
- {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
- {"/path/%u0025../info", "/path/%../info", EnumSet.of(Violation.ENCODING, Violation.UTF16)},
+ {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING)},
+ {"/path/%u0025/info", "/path/%/info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING, Violation.UTF16_ENCODINGS)},
+ {"%25/info", "%/info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING)},
+ {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING)},
+ {"/path/%u0025../info", "/path/%../info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING, Violation.UTF16_ENCODINGS)},
// combinations
- {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
- {"/path/%u002f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.UTF16)},
- {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
+ {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/%u002f/..;/info", "/path//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_PARAMETER, Violation.UTF16_ENCODINGS)},
+ {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_PARAMETER, Violation.AMBIGUOUS_PATH_SEGMENT)},
// Non ascii characters
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
@@ -462,15 +462,15 @@ public void testDecodedPath(String input, String decodedPath, EnumSet
HttpURI uri = HttpURI.from(input);
assertThat(uri.getDecodedPath(), is(decodedPath));
EnumSet ambiguous = EnumSet.copyOf(expected);
- ambiguous.retainAll(EnumSet.complementOf(EnumSet.of(Violation.UTF16)));
+ ambiguous.retainAll(EnumSet.complementOf(EnumSet.of(Violation.UTF16_ENCODINGS)));
assertThat(uri.isAmbiguous(), is(!ambiguous.isEmpty()));
- assertThat(uri.hasAmbiguousSegment(), is(ambiguous.contains(Violation.SEGMENT)));
- assertThat(uri.hasAmbiguousSeparator(), is(ambiguous.contains(Violation.SEPARATOR)));
- assertThat(uri.hasAmbiguousParameter(), is(ambiguous.contains(Violation.PARAM)));
- assertThat(uri.hasAmbiguousEncoding(), is(ambiguous.contains(Violation.ENCODING)));
+ assertThat(uri.hasAmbiguousSegment(), is(ambiguous.contains(Violation.AMBIGUOUS_PATH_SEGMENT)));
+ assertThat(uri.hasAmbiguousSeparator(), is(ambiguous.contains(Violation.AMBIGUOUS_PATH_SEPARATOR)));
+ assertThat(uri.hasAmbiguousParameter(), is(ambiguous.contains(Violation.AMBIGUOUS_PATH_PARAMETER)));
+ assertThat(uri.hasAmbiguousEncoding(), is(ambiguous.contains(Violation.AMBIGUOUS_PATH_ENCODING)));
- assertThat(uri.hasUtf16Encoding(), is(expected.contains(Violation.UTF16)));
+ assertThat(uri.hasUtf16Encoding(), is(expected.contains(Violation.UTF16_ENCODINGS)));
}
catch (Exception e)
{
@@ -507,69 +507,69 @@ public static Stream testPathQueryTests()
{"..;param/info", null, null},
// ambiguous dot encodings
- {"/path/%2e/info", "/path/info", EnumSet.of(Violation.SEGMENT)},
- {"path/%2e/info/", "path/info/", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.SEGMENT)},
- {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.SEGMENT)},
- {"%2e/info", "info", EnumSet.of(Violation.SEGMENT)},
- {"%2e", "", EnumSet.of(Violation.SEGMENT)},
+ {"/path/%2e/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"path/%2e/info/", "path/info/", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e;/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e;param/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2e%2e;param;other/info;other", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"%2e/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"%2e", "", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT)},
// empty segment treated as ambiguous
{"/", "/", EnumSet.noneOf(Violation.class)},
{"/#", "/", EnumSet.noneOf(Violation.class)},
{"/path", "/path", EnumSet.noneOf(Violation.class)},
{"/path/", "/path/", EnumSet.noneOf(Violation.class)},
- {"//", "//", EnumSet.of(Violation.EMPTY)},
- {"/foo//", "/foo//", EnumSet.of(Violation.EMPTY)},
- {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
- {"//foo/bar", "//foo/bar", EnumSet.of(Violation.EMPTY)},
+ {"//", "//", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo//", "/foo//", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo//bar", "/foo//bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"//foo/bar", "//foo/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"/foo?bar", "/foo", EnumSet.noneOf(Violation.class)},
{"/foo#bar", "/foo", EnumSet.noneOf(Violation.class)},
{"/foo;bar", "/foo", EnumSet.noneOf(Violation.class)},
{"/foo/?bar", "/foo/", EnumSet.noneOf(Violation.class)},
{"/foo/#bar", "/foo/", EnumSet.noneOf(Violation.class)},
{"/foo/;param", "/foo/", EnumSet.noneOf(Violation.class)},
- {"/foo/;param/bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
- {"/foo//bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
- {"/foo//bar//", "/foo//bar//", EnumSet.of(Violation.EMPTY)},
- {"//foo//bar//", "//foo//bar//", EnumSet.of(Violation.EMPTY)},
- {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.EMPTY)},
- {"/foo///../../../bar", "/bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo/;param/bar", "/foo//bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo//bar", "/foo//bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo//bar//", "/foo//bar//", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"//foo//bar//", "//foo//bar//", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo//../bar", "/foo/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {"/foo///../../../bar", "/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"/foo/./../bar", "/bar", EnumSet.noneOf(Violation.class)},
- {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.EMPTY)},
+ {"/foo//./bar", "/foo//bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"foo/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
{"foo;/bar", "foo/bar", EnumSet.noneOf(Violation.class)},
- {";/bar", "/bar", EnumSet.of(Violation.EMPTY)},
- {";?n=v", "", EnumSet.of(Violation.EMPTY)},
+ {";/bar", "/bar", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
+ {";?n=v", "", EnumSet.of(Violation.AMBIGUOUS_EMPTY_SEGMENT)},
{"?n=v", "", EnumSet.noneOf(Violation.class)},
{"#n=v", "", EnumSet.noneOf(Violation.class)},
{"", "", EnumSet.noneOf(Violation.class)},
// ambiguous parameter inclusions
- {"/path/.;/info", "/path/info", EnumSet.of(Violation.PARAM)},
- {"/path/.;param/info", "/path/info", EnumSet.of(Violation.PARAM)},
- {"/path/..;/info", "/info", EnumSet.of(Violation.PARAM)},
- {"/path/..;param/info", "/info", EnumSet.of(Violation.PARAM)},
- {".;/info", "info", EnumSet.of(Violation.PARAM)},
- {".;param/info", "info", EnumSet.of(Violation.PARAM)},
+ {"/path/.;/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/.;param/info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/..;/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/..;param/info", "/info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {".;/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {".;param/info", "info", EnumSet.of(Violation.AMBIGUOUS_PATH_PARAMETER)},
// ambiguous segment separators
- {"/path/%2f/info", "/path///info", EnumSet.of(Violation.SEPARATOR)},
- {"%2f/info", "//info", EnumSet.of(Violation.SEPARATOR)},
- {"%2F/info", "//info", EnumSet.of(Violation.SEPARATOR)},
- {"/path/%2f../info", "/path/info", EnumSet.of(Violation.SEPARATOR)},
+ {"/path/%2f/info", "/path///info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
+ {"%2f/info", "//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
+ {"%2F/info", "//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
+ {"/path/%2f../info", "/path/info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR)},
// ambiguous encoding
- {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.ENCODING)},
- {"%25/info", "%/info", EnumSet.of(Violation.ENCODING)},
- {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.ENCODING)},
+ {"/path/%25/info", "/path/%/info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING)},
+ {"%25/info", "%/info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING)},
+ {"/path/%25../info", "/path/%../info", EnumSet.of(Violation.AMBIGUOUS_PATH_ENCODING)},
// combinations
- {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM)},
- {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT)},
- {"/path/%2f/%25/..;/%2e//info", "/path////info", EnumSet.of(Violation.SEPARATOR, Violation.PARAM, Violation.SEGMENT, Violation.ENCODING, Violation.EMPTY)},
+ {"/path/%2f/..;/info", "/path//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_PARAMETER)},
+ {"/path/%2f/..;/%2e/info", "/path//info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_PARAMETER, Violation.AMBIGUOUS_PATH_SEGMENT)},
+ {"/path/%2f/%25/..;/%2e//info", "/path////info", EnumSet.of(Violation.AMBIGUOUS_PATH_SEPARATOR, Violation.AMBIGUOUS_PATH_PARAMETER, Violation.AMBIGUOUS_PATH_SEGMENT, Violation.AMBIGUOUS_PATH_ENCODING, Violation.AMBIGUOUS_EMPTY_SEGMENT)},
}).map(Arguments::of);
}
@@ -587,11 +587,11 @@ public void testPathQuery(String input, String decodedPath, EnumSet e
HttpURI uri = HttpURI.build().pathQuery(input);
assertThat(uri.getDecodedPath(), is(decodedPath));
assertThat(uri.isAmbiguous(), is(!expected.isEmpty()));
- assertThat(uri.hasAmbiguousEmptySegment(), is(expected.contains(Violation.EMPTY)));
- assertThat(uri.hasAmbiguousSegment(), is(expected.contains(Violation.SEGMENT)));
- assertThat(uri.hasAmbiguousSeparator(), is(expected.contains(Violation.SEPARATOR)));
- assertThat(uri.hasAmbiguousParameter(), is(expected.contains(Violation.PARAM)));
- assertThat(uri.hasAmbiguousEncoding(), is(expected.contains(Violation.ENCODING)));
+ assertThat(uri.hasAmbiguousEmptySegment(), is(expected.contains(Violation.AMBIGUOUS_EMPTY_SEGMENT)));
+ assertThat(uri.hasAmbiguousSegment(), is(expected.contains(Violation.AMBIGUOUS_PATH_SEGMENT)));
+ assertThat(uri.hasAmbiguousSeparator(), is(expected.contains(Violation.AMBIGUOUS_PATH_SEPARATOR)));
+ assertThat(uri.hasAmbiguousParameter(), is(expected.contains(Violation.AMBIGUOUS_PATH_PARAMETER)));
+ assertThat(uri.hasAmbiguousEncoding(), is(expected.contains(Violation.AMBIGUOUS_PATH_ENCODING)));
}
public static Stream parseData()
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 f496fa782c63..9e4735621c3a 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
@@ -2222,6 +2222,7 @@ public RequestDispatcher getRequestDispatcher(String uriInContext)
try
{
String contextPath = getContextPath();
+ // uriInContext is canonicalized by HttpURI.
HttpURI.Mutable uri = HttpURI.build(uriInContext);
String pathInfo = uri.getDecodedPath();
if (StringUtil.isEmpty(pathInfo))
@@ -2244,6 +2245,10 @@ public RequestDispatcher getRequestDispatcher(String uriInContext)
@Override
public String getRealPath(String path)
{
+ // 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;
if (path.length() == 0)
@@ -2308,6 +2313,10 @@ public InputStream getResourceAsStream(String path)
@Override
public Set getResourcePaths(String path)
{
+ // 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;
return ContextHandler.this.getResourcePaths(path);