Issue #6473 - canonicalPath refactor & fix alias check in PathResource (Jetty-10) (#6478)

Issue #6473 - canonicalPath refactor & fix alias check 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
* Compliance mode HttpURI uses UriCompliance.Violation

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
Co-authored-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Lachlan 2021-06-29 23:42:39 +10:00 committed by GitHub
parent c753ca0db5
commit 3c32afa05c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 768 additions and 516 deletions

View File

@ -15,8 +15,11 @@ package org.eclipse.jetty.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
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;
@ -31,56 +34,48 @@ import org.eclipse.jetty.util.UrlEncoded;
* via the static methods such as {@link #build()} and {@link #from(String)}.
*
* A URI such as
* <code>http://user@host:port/path;ignored/info;param?query#ignored</code>
* is split into the following undecoded elements:<ul>
* {@code http://user@host:port/path;param1/%2e/info;param2?query#fragment}
* is split into the following optional elements:<ul>
* <li>{@link #getScheme()} - http:</li>
* <li>{@link #getAuthority()} - //name@host:port</li>
* <li>{@link #getHost()} - host</li>
* <li>{@link #getPort()} - port</li>
* <li>{@link #getPath()} - /path/info</li>
* <li>{@link #getParam()} - param</li>
* <li>{@link #getPath()} - /path;param1/%2e/info;param2</li>
* <li>{@link #getDecodedPath()} - /path/info</li>
* <li>{@link #getParam()} - param2</li>
* <li>{@link #getQuery()} - query</li>
* <li>{@link #getFragment()} - fragment</li>
* </ul>
* <p>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.
*/
* <p>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 <a href="https://tools.ietf.org/html/rfc3986#section-3.3">RFC3986</a>
* which no longer defines path parameters (removed after
* <a href="https://tools.ietf.org/html/rfc2396#section-3.3">RFC2396</a>) 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.
* </p>
* <p>
* 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 #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.
* </p>
* <p>
* 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()}.
* </p>
* <p>
* If there are multiple path parameters, only the last one is returned by {@link #getParam()}.
* </p>
**/
public interface HttpURI
{
enum Violation
{
/**
* URI contains ambiguous path segments e.g. {@code /foo/%2e%2e/bar}
*/
SEGMENT,
/**
* URI contains ambiguous empty segments e.g. {@code /foo//bar} or {@code /foo/;param/}, but not {@code /foo/}
*/
EMPTY,
/**
* URI contains ambiguous path separator within a URI segment e.g. {@code /foo/b%2fr}
*/
SEPARATOR,
/**
* URI contains ambiguous path encoding within a URI segment e.g. {@code /%2557EB-INF}
*/
ENCODING,
/**
* URI contains ambiguous path parameters within a URI segment e.g. {@code /foo/..;/bar}
*/
PARAM,
/**
* Contains UTF16 encodings
*/
UTF16
}
static Mutable build()
{
return new Mutable();
@ -147,6 +142,11 @@ public interface HttpURI
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();
@ -166,41 +166,73 @@ public interface HttpURI
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();
/**
* @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<Violation> getViolations();
/**
* @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()
{
@ -408,7 +440,7 @@ public interface HttpURI
@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
@ -418,39 +450,15 @@ public interface HttpURI
}
@Override
public boolean hasAmbiguousSegment()
public boolean hasViolation(Violation violation)
{
return _violations.contains(Violation.SEGMENT);
return _violations.contains(violation);
}
@Override
public boolean hasAmbiguousEmptySegment()
public Collection<Violation> getViolations()
{
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
@ -781,7 +789,7 @@ public interface HttpURI
@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
@ -791,39 +799,15 @@ public interface HttpURI
}
@Override
public boolean hasAmbiguousSegment()
public boolean hasViolation(Violation violation)
{
return _violations.contains(Violation.SEGMENT);
return _violations.contains(violation);
}
@Override
public boolean hasAmbiguousEmptySegment()
public Collection<Violation> getViolations()
{
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()
@ -928,9 +912,9 @@ public interface HttpURI
_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;
}
@ -1205,7 +1189,7 @@ public interface HttpURI
{
if (encodedCharacters == 2 && c == 'u' && !encodedUtf16)
{
_violations.add(Violation.UTF16);
_violations.add(Violation.UTF16_ENCODINGS);
encodedUtf16 = true;
encodedCharacters = 4;
continue;
@ -1216,11 +1200,15 @@ public interface HttpURI
{
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);
_violations.add(Violation.AMBIGUOUS_PATH_SEPARATOR);
break;
case '%':
_violations.add(Violation.ENCODING);
_violations.add(Violation.AMBIGUOUS_PATH_ENCODING);
break;
default:
break;
@ -1301,26 +1289,11 @@ public interface HttpURI
}
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;
}
@ -1335,7 +1308,9 @@ public interface HttpURI
break;
}
default:
{
throw new IllegalStateException(state.toString());
}
}
}
@ -1387,10 +1362,12 @@ public interface HttpURI
}
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 decodedNonCanonical = URIUtil.decodePath(_path);
_decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
if (_decodedPath == null)
throw new IllegalArgumentException("Bad URI");
}
}
@ -1409,9 +1386,10 @@ public interface HttpURI
// 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);
_violations.add(Violation.AMBIGUOUS_EMPTY_SEGMENT);
if (end == segment)
{
@ -1422,7 +1400,7 @@ public interface HttpURI
// 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;
}
@ -1439,12 +1417,12 @@ public interface HttpURI
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);
}
}
}

View File

@ -25,7 +25,6 @@ import org.slf4j.LoggerFactory;
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;
@ -67,10 +66,6 @@ public final class UriCompliance implements ComplianceViolation.Mode
* Allow ambiguous path encoding within a URI segment e.g. <code>/%2557EB-INF</code>
*/
AMBIGUOUS_PATH_ENCODING("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path encoding"),
/**
* Allow Non canonical ambiguous paths. eg <code>/foo/x%2f%2e%2e%/bar</code> provided to applications as <code>/foo/x/../bar</code>
*/
NON_CANONICAL_AMBIGUOUS_PATHS("https://tools.ietf.org/html/rfc3986#section-3.3", "Non canonical ambiguous paths"),
/**
* Allow UTF-16 encoding eg <code>/foo%u2192bar</code>.
*/
@ -125,10 +120,9 @@ public final class UriCompliance implements ComplianceViolation.Mode
/**
* Compliance mode that exactly follows <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>,
* 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 <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>
@ -231,6 +225,11 @@ public final class UriCompliance implements ComplianceViolation.Mode
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 +329,14 @@ public final class UriCompliance implements ComplianceViolation.Mode
return EnumSet.noneOf(Violation.class);
return EnumSet.copyOf(violations);
}
public static String checkUriCompliance(UriCompliance compliance, HttpURI uri)
{
for (UriCompliance.Violation violation : UriCompliance.Violation.values())
{
if (uri.hasViolation(violation) && (compliance == null || !compliance.allows(violation)))
return violation.getDescription();
}
return null;
}
}

View File

@ -19,7 +19,7 @@ import java.util.Arrays;
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;
@ -36,6 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
public class HttpURITest
{
@Test
@ -146,6 +147,12 @@ public class HttpURITest
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
@ -350,7 +357,9 @@ public class HttpURITest
// encoded paths
{"/f%6f%6F/bar", "/foo/bar", EnumSet.noneOf(Violation.class)},
{"/f%u006f%u006F/bar", "/foo/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)},
@ -360,75 +369,81 @@ public class HttpURITest
{"/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)},
{"/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)},
{"%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.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", "/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.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
@ -447,15 +462,15 @@ public class HttpURITest
HttpURI uri = HttpURI.from(input);
assertThat(uri.getDecodedPath(), is(decodedPath));
EnumSet<Violation> 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)
{
@ -483,78 +498,78 @@ public class HttpURITest
{"../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.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", "/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.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);
}
@ -572,11 +587,11 @@ public class HttpURITest
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<Arguments> parseData()
@ -793,4 +808,20 @@ public class HttpURITest
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(javaUri.getFragment()));
assertThat("[" + input + "] .toString", httpUri.toString(), is(javaUri.toASCIIString()));
}
public static Stream<Arguments> 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));
}
}

View File

@ -361,6 +361,7 @@ public class MavenWebAppContext extends WebAppContext
// /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;

View File

@ -40,20 +40,20 @@ public final class RedirectUtil
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));
if (!location.startsWith("/"))
location = URIUtil.canonicalURI(URIUtil.addEncodedPaths(parent, location));
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();

View File

@ -16,11 +16,11 @@ package org.eclipse.jetty.rewrite.handler;
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;
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")
@ -62,7 +62,7 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
{
_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);
@ -72,10 +72,17 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
@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");
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/jsp/bean1.jsp%00"));
_request.setHttpURI(HttpURI.from("/jsp/bean1.jsp\000"));
String result = _rule.matchAndApply(_request.getRequestURI(), _request, _response);
@ -86,14 +93,7 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
@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"));
String result = _rule.matchAndApply(_request.getRequestURI(), _request, _response);
assertEquals(405, _response.getStatus());
assertEquals("foo", _request.getAttribute(Dispatcher.ERROR_MESSAGE));
assertThrows(IllegalArgumentException.class, () -> HttpURI.build(_request.getHttpURI(), "/jsp/shamrock-%00%E2%98%98.jsp"));
}
@Test
@ -119,4 +119,3 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase
//@checkstyle-enable-check : IllegalTokenText
}
}

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.server;
import java.io.IOException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
@ -27,6 +28,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.MultiMap;
@ -76,7 +78,7 @@ public class Dispatcher implements RequestDispatcher
@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 +100,10 @@ public class Dispatcher implements RequestDispatcher
}
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 +137,7 @@ public class Dispatcher implements RequestDispatcher
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 +165,10 @@ public class Dispatcher implements RequestDispatcher
}
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
@ -230,6 +240,18 @@ public class Dispatcher implements RequestDispatcher
}
}
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()
{

View File

@ -67,6 +67,7 @@ import javax.servlet.http.WebConnection;
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 class Request implements HttpServletRequest
_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 class Request implements HttpServletRequest
}
_uri = builder.asImmutable();
}
setSecure(HttpScheme.HTTPS.is(_uri.getScheme()));
String encoded = _uri.getPath();
@ -1751,17 +1736,15 @@ public class Request implements HttpServletRequest
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())
{

View File

@ -568,14 +568,14 @@ public class Response implements HttpServletResponse
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('/');
}

View File

@ -29,7 +29,6 @@ import java.util.Enumeration;
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 class ContextHandler extends ScopedHandler implements Attributes, Gracefu
return false;
if (target.startsWith("//"))
// ignore empty segments which may be discard by file system
target = URIUtil.compactPath(target);
ProtectedTargetType type = _protectedTargets.getBest(target);
@ -1930,7 +1928,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
try
{
path = URIUtil.canonicalPath(path);
// 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))
@ -1959,9 +1959,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
// alias checks
for (Iterator<AliasCheck> i = getAliasChecks().iterator(); i.hasNext(); )
for (AliasCheck check : getAliasChecks())
{
AliasCheck check = i.next();
if (check.check(path, resource))
{
if (LOG.isDebugEnabled())
@ -2014,7 +2013,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
{
try
{
path = URIUtil.canonicalPath(path);
Resource resource = getResource(path);
if (resource != null && resource.exists())
@ -2214,6 +2212,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@Override
public RequestDispatcher getRequestDispatcher(String uriInContext)
{
// uriInContext is encoded, potentially with query.
if (uriInContext == null)
return null;
@ -2223,10 +2222,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
try
{
String contextPath = getContextPath();
String pathInfo;
// uriInContext is canonicalized by HttpURI.
HttpURI.Mutable uri = HttpURI.build(uriInContext);
pathInfo = URIUtil.canonicalPath(uri.getDecodedPath());
String pathInfo = uri.getDecodedPath();
if (StringUtil.isEmpty(pathInfo))
return null;
@ -2247,6 +2245,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@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)
@ -2275,6 +2277,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@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);
if (resource != null && resource.exists())
return resource.getURI().toURL();
@ -2305,6 +2313,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@Override
public Set<String> 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);
}

View File

@ -156,7 +156,6 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
if (_baseResource != null)
{
path = URIUtil.canonicalPath(path);
r = _baseResource.addPath(path);
if (r.isAlias() && (_context == null || !_context.checkAlias(path, r)))
@ -169,8 +168,6 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
else if (_context != null)
{
r = _context.getResource(path);
if (r != null)
return r;
}
if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))

View File

@ -504,7 +504,7 @@ public class HttpConnectionTest
{
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 class HttpConnectionTest
{
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 class HttpConnectionTest
{
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
@ -827,7 +827,7 @@ public class HttpConnectionTest
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n");
checkContains(response, 0, "HTTP/1.1 200"); //now fallback to iso-8859-1
checkContains(response, 0, "HTTP/1.1 400");
response = connector.getResponse("GET /bad/utf8%c1 HTTP/1.1\r\n" +
"Host: localhost\r\n" +

View File

@ -1713,15 +1713,6 @@ public class RequestTest
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

View File

@ -28,6 +28,8 @@ import org.junit.jupiter.api.BeforeAll;
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,39 @@ public class ContextHandlerGetResourceTest
}
@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/.././../";
String path = "/./index.html";
Resource resource = context.getResource(path);
assertNull(resource);
URL resourceURL = context.getServletContext().getResource(path);
assertFalse(resourceURL.getPath().contains("/./"));
path = "/down/../index.html";
resource = context.getResource(path);
assertNull(resource);
resourceURL = context.getServletContext().getResource(path);
assertFalse(resourceURL.getPath().contains("/../"));
path = "//index.html";
resource = context.getResource(path);
assertNull(resource);
resourceURL = context.getServletContext().getResource(path);
assertNull(resourceURL);
}
@ParameterizedTest
@ValueSource(strings = {"/down/.././../", "/../down/"})
public void testNormalize(String path) throws Exception
{
URL url = context.getServletContext().getResource(path);
assertNull(url);
}

View File

@ -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 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc
}
else
{
URL u = _servletContext.getResource(pathInContext);
r = _contextHandler.newResource(u);
return null;
}
if (LOG.isDebugEnabled())

View File

@ -57,11 +57,13 @@ public class RequestURITest
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)
@ -69,8 +71,13 @@ public class RequestURITest
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)
for (int i = 0x0; i < 0x1f; i++)
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++)
{
String raw = String.format("/hello%%%02Xworld", i);
ret.add(Arguments.of(raw, raw, null));
@ -194,7 +201,6 @@ public class RequestURITest
// 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));
@ -221,4 +227,43 @@ public class RequestURITest
assertThat(response, Matchers.containsString("QueryString: " + expectedQuery));
}
}
public static Stream<Arguments> badData()
{
List<Arguments> 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/%#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 "));
}
}
}

View File

@ -34,7 +34,7 @@ import org.eclipse.jetty.server.HttpOutput;
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.
* <p>
* Two implementations are supported: <ul>
@ -42,8 +42,7 @@ import org.eclipse.jetty.util.ProcessorUtils;
* APIs, but produces more garbage due to the byte[] nature of the API.
* <li>the <code>JettyDataStream</code> 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.
* </ul>
* <p>
* The data rate is controlled by setting init parameters:
@ -53,7 +52,9 @@ import org.eclipse.jetty.util.ProcessorUtils;
* <dt>pool</dt><dd>The size of the thread pool used to service the writes (defaults to available processors)</dd>
* </dl>
* 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;

View File

@ -57,6 +57,7 @@ import org.eclipse.jetty.util.URIUtil;
* <li><b>putAtomic</b> - boolean, if true PUT files are written to a temp location and moved into place.
* </ul>
*/
@Deprecated
public class PutFilter implements Filter
{
public static final String __PUT = "PUT";
@ -80,7 +81,8 @@ public class PutFilter implements Filter
_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 class PutFilter implements Filter
}
else
{
File base = new File(_context.getRealPath("/"));
File base = new File(realPath);
_baseURI = base.toURI().toString();
}
@ -284,7 +286,7 @@ public class PutFilter implements Filter
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);

View File

@ -471,6 +471,7 @@ public class URIUtil
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;
}
@ -532,7 +533,6 @@ public class URIUtil
{
throw new IllegalArgumentException("cannot decode URI", e);
}
}
/* Decode a URI path and strip parameters of ISO-8859-1 path
@ -776,143 +776,138 @@ public class URIUtil
}
/**
* Convert an encoded path to a canonical form.
* Convert a partial URI to a canonical form.
* <p>
* 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.
* </p>
*
* @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)
*/
public static String canonicalPath(String path)
public static String canonicalURI(String uri)
{
// See https://tools.ietf.org/html/rfc3986#section-5.2.4
if (uri == null || uri.isEmpty())
return uri;
if (path == null || path.isEmpty())
return path;
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.
* <p>
* All instances of "." and ".." are factored out.
* </p>
* @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.
* <p>
* All segments of "." and ".." are factored out.
* Null is returned if the path tries to .. above its root.
* </p>
*
* @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 +916,8 @@ public class URIUtil
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 +932,6 @@ public class URIUtil
slash = false;
break;
case '?':
return path;
default:
slash = false;
}
@ -947,56 +939,31 @@ public class URIUtil
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 +974,66 @@ public class URIUtil
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 "/"

View File

@ -213,7 +213,6 @@ public abstract class Utf8Appendable
protected void appendByte(byte b) throws IOException
{
if (b > 0 && _state == UTF8_ACCEPT)
{
_appendable.append((char)(b & 0xFF));

View File

@ -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 @@ public class PathResource extends Resource
* we will just use the original URI to construct the
* alias reference Path.
*/
if (!URIUtil.equalsIgnoreEncodings(uri, path.toUri()))
{
try
@ -85,9 +84,14 @@ public class PathResource extends Resource
}
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 (!isSameName(abs, normal))
return normal;
try
{
@ -96,11 +100,8 @@ public class PathResource extends Resource
if (Files.exists(path))
{
Path real = abs.toRealPath(FOLLOW_LINKS);
if (!isSameName(abs, real))
{
return real;
}
}
}
catch (IOException e)
@ -233,8 +234,7 @@ public class PathResource extends Resource
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 @@ public class PathResource extends Resource
// 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 += "/";
@ -348,22 +348,23 @@ public class PathResource extends Resource
}
@Override
public Resource addPath(final String subpath) throws IOException
public Resource addPath(final String subPath) throws IOException
{
String cpath = URIUtil.canonicalPath(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 ((cpath == null) || (cpath.length() == 0))
throw new MalformedURLException(subpath);
if ("/".equals(cpath))
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)

View File

@ -419,10 +419,12 @@ public abstract class Resource implements ResourceFactory, Closeable
* 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,7 @@ public abstract class Resource implements ResourceFactory, Closeable
*/
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;

View File

@ -280,9 +280,11 @@ public class URLResource extends Resource
public Resource addPath(String path)
throws IOException
{
Objects.requireNonNull(path, "Path may not be null");
path = URIUtil.canonicalPath(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);
}

View File

@ -22,10 +22,11 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
public class URIUtilCanonicalPathTest
{
public static Stream<Arguments> data()
public static Stream<Arguments> paths()
{
String[][] canonical =
{
@ -83,6 +84,7 @@ public class URIUtilCanonicalPathTest
{"/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 class URIUtilCanonicalPathTest
{"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 class URIUtilCanonicalPathTest
{"/%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,43 @@ public class URIUtilCanonicalPathTest
}
@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/."));
}
}
public static Stream<Arguments> 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));
}
}

View File

@ -151,6 +151,35 @@ public class Utf8AppendableTest
});
}
@ParameterizedTest
@MethodSource("implementations")
public void testInvalidZeroUTF8(Class<Utf8Appendable> 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<Utf8Appendable> 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<Utf8Appendable> impl) throws Exception

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.util.resource;
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.equalTo;
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 class ResourceTest
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"));
}
}

View File

@ -400,7 +400,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
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 class WebAppContext extends ServletContextHandler implements WebAppClassL
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 class WebAppContext extends ServletContextHandler implements WebAppClassL
@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;

View File

@ -53,6 +53,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
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 class WebAppContextTest
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

View File

@ -22,8 +22,12 @@ import java.util.ArrayList;
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 class JspAndDefaultWithoutAliasesTest
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 class JspAndDefaultWithoutAliasesTest
// 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();