Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #6473 - Improve alias checking in PathResource. #6477

Merged
merged 4 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 30 additions & 37 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/**
* Http URI.
* Parse an HTTP URI from a string or byte array. Given a URI
* <code>http://user@host:port/path;param1/%2e/info;param2?query#fragment</code>
* {@code http://user@host:port/path;param1/%2e/info;param2?query#fragment}
* this class will split it into the following optional elements:<ul>
* <li>{@link #getScheme()} - http:</li>
* <li>{@link #getAuthority()} - //name@host:port</li>
Expand Down Expand Up @@ -65,11 +65,13 @@
* Thus this class avoid and/or detects such ambiguities. Furthermore, by decoding characters and
* removing parameters before relative path normalization, ambiguous paths will be resolved in such
* a way to be non-standard-but-non-ambiguous to down stream interpretation of the decoded path string.
* The violations are recorded and available by API such as {@link #hasViolation(Violation)} so that requests
* The violations are recorded and available by API such as {@link #hasAmbiguousSegment()} so that requests
* containing them may be rejected in case the non-standard-but-non-ambiguous interpretations
* are not satisfactory for a given compliance configuration. Implementations that wish to
* process ambiguous URI paths must configure the compliance modes to accept them and then perform
* their own decoding of {@link #getPath()}.
* are not satisfactory for a given compliance configuration.
* </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()}.
Expand All @@ -95,30 +97,30 @@ private enum State
/**
* Violations of safe URI interpretations
*/
public enum Violation
enum Violation
{
/**
* Ambiguous path segments e.g. <code>/foo/%2E%2E/bar</code>
* 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</code>
* 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</code>
* 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</code>
* Ambiguous double encoding within a URI segment e.g. {@code /%2557EB-INF}
*/
ENCODING("Ambiguous double encoding"),
/**
* Ambiguous empty segments e.g. <code>/foo//bar</code>
* Ambiguous empty segments e.g. {@code /foo//bar}
*/
EMPTY("Ambiguous empty segments"),
/**
* Non standard UTF-16 encoding eg <code>/foo%u2192bar</code>.
* Non standard UTF-16 encoding eg {@code /foo%u2192bar}.
*/
UTF16("Non standard UTF-16 encoding");

Expand Down Expand Up @@ -338,7 +340,7 @@ private void parse(State state, final String uri, final int offset, final int en
int encodedValue = 0; // the partial encoded value
boolean dot = false; // set to true if the path contains . or .. segments

for (int i = 0; i < end; i++)
for (int i = offset; i < end; i++)
{
char c = uri.charAt(i);

Expand Down Expand Up @@ -567,6 +569,8 @@ else if (c == '/')
switch (encodedValue)
{
case 0:
// Byte 0 cannot be present in a UTF-8 sequence in any position
// other than as the NUL ASCII byte which we do not wish to allow.
throw new IllegalArgumentException("Illegal character in path");
case '/':
_violations.add(Violation.SEPARATOR);
Expand Down Expand Up @@ -653,26 +657,11 @@ else if (c == '/')
}
case QUERY:
{
switch (c)
if (c == '#')
{
case '%':
encodedCharacters = 2;
break;
case 'u':
case 'U':
if (encodedCharacters == 1)
_violations.add(Violation.UTF16);
encodedCharacters = 0;
break;
case '#':
_query = uri.substring(mark, i);
mark = i + 1;
state = State.FRAGMENT;
encodedCharacters = 0;
break;
default:
encodedCharacters = 0;
break;
_query = uri.substring(mark, i);
mark = i + 1;
state = State.FRAGMENT;
}
break;
}
Expand All @@ -687,7 +676,9 @@ else if (c == '/')
break;
}
default:
{
throw new IllegalStateException(state.toString());
}
}
}

Expand Down Expand Up @@ -741,8 +732,8 @@ else if (_path != null)
{
// The RFC requires this to be canonical before decoding, but this can leave dot segments and dot dot segments
// which are not canonicalized and could be used in an attempt to bypass security checks.
String decodeNonCanonical = URIUtil.decodePath(_path);
_decodedPath = URIUtil.canonicalPath(decodeNonCanonical);
String decodedNonCanonical = URIUtil.decodePath(_path);
_decodedPath = URIUtil.canonicalPath(decodedNonCanonical);
if (_decodedPath == null)
throw new IllegalArgumentException("Bad URI");
}
Expand All @@ -763,7 +754,8 @@ private void checkSegment(String uri, int segment, int end, boolean param)
// This method is called once for every segment parsed.
// A URI like "/foo/" has two segments: "foo" and an empty segment.
// Empty segments are only ambiguous if they are not the last segment
// So if this method is called for any segment and we have previously seen an empty segment, then it was ambiguous
// So if this method is called for any segment and we have previously
// seen an empty segment, then it was ambiguous.
if (_emptySegment)
_violations.add(Violation.EMPTY);

Expand Down Expand Up @@ -843,7 +835,8 @@ public boolean hasAmbiguousEncoding()
}

/**
* @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousSeparator()}.
* @return True if the URI has either an {@link #hasAmbiguousSegment()} or {@link #hasAmbiguousEmptySegment()}
* or {@link #hasAmbiguousSeparator()} or {@link #hasAmbiguousParameter()}
*/
public boolean isAmbiguous()
{
Expand All @@ -858,7 +851,7 @@ public boolean hasViolations()
return !_violations.isEmpty();
}

public boolean hasViolation(Violation violation)
boolean hasViolation(Violation violation)
{
return _violations.contains(violation);
}
Expand Down
20 changes: 20 additions & 0 deletions jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public static Stream<Arguments> 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)},
Expand All @@ -333,6 +334,9 @@ public static Stream<Arguments> 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)},
Expand Down Expand Up @@ -769,4 +773,20 @@ public void testCompareToJavaNetURI(String input, String scheme, String host, In
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(javaUri.getFragment()));
assertThat("[" + input + "] .toString", httpUri.toString(), is(javaUri.toASCIIString()));
}

public static Stream<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 = new HttpURI(input);
assertThat("[" + input + "] .query", httpURI.getQuery(), is(expectedQuery));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
Expand Down Expand Up @@ -450,12 +451,17 @@ public Resource getResource(String uriInContext) throws MalformedURLException
// If no regular resource exists check for access to /WEB-INF/lib or /WEB-INF/classes
if ((resource == null || !resource.exists()) && uriInContext != null && _classes != null)
{
// Canonicalize again to look for the resource inside /WEB-INF subdirectories.
String uri = URIUtil.canonicalPath(uriInContext);
if (uri == null)
return null;

try
{
// Replace /WEB-INF/classes with candidates for the classpath
if (uriInContext.startsWith(WEB_INF_CLASSES_PREFIX))
if (uri.startsWith(WEB_INF_CLASSES_PREFIX))
{
if (uriInContext.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX) || uriInContext.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX + "/"))
if (uri.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX) || uri.equalsIgnoreCase(WEB_INF_CLASSES_PREFIX + "/"))
{
//exact match for a WEB-INF/classes, so preferentially return the resource matching the web-inf classes
//rather than the test classes
Expand All @@ -471,7 +477,7 @@ else if (_testClasses != null)
int i = 0;
while (res == null && (i < _webInfClasses.size()))
{
String newPath = StringUtil.replace(uriInContext, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
String newPath = StringUtil.replace(uri, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
res = Resource.newResource(newPath);
if (!res.exists())
{
Expand All @@ -482,11 +488,11 @@ else if (_testClasses != null)
return res;
}
}
else if (uriInContext.startsWith(WEB_INF_LIB_PREFIX))
else if (uri.startsWith(WEB_INF_LIB_PREFIX))
{
// Return the real jar file for all accesses to
// /WEB-INF/lib/*.jar
String jarName = StringUtil.strip(uriInContext, WEB_INF_LIB_PREFIX);
String jarName = StringUtil.strip(uri, WEB_INF_LIB_PREFIX);
if (jarName.startsWith("/") || jarName.startsWith("\\"))
jarName = jarName.substring(1);
if (jarName.length() == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ public static String toRedirectURL(final HttpServletRequest request, String loca
String path = request.getRequestURI();
String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path);
location = URIUtil.canonicalURI(URIUtil.addEncodedPaths(parent, location));
if (!location.startsWith("/"))
if (location != null && !location.startsWith("/"))
url.append('/');
}

if (location == null)
throw new IllegalStateException("path cannot be above root");
throw new IllegalStateException("redirect path cannot be above root");
url.append(location);

location = url.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SuppressWarnings("unused")
Expand Down Expand Up @@ -75,6 +76,12 @@ public void testInvalidUrlWithReason() throws Exception

@Test
public void testInvalidJsp() throws Exception
{
assertThrows(IllegalArgumentException.class, () -> _request.setURIPathQuery("/jsp/bean1.jsp%00"));
}

@Test
public void testInvalidJspWithNullByte() throws Exception
{
_rule.setCode("405");
_rule.setReason("foo");
Expand All @@ -87,6 +94,12 @@ public void testInvalidJsp() throws Exception
assertEquals("foo", _response.getReason());
}

@Test
public void testInvalidShamrock() throws Exception
{
assertThrows(IllegalArgumentException.class, () -> _request.setURIPathQuery("/jsp/shamrock-%00%E2%98%98.jsp"));
}

@Test
public void testValidShamrock() throws Exception
{
Expand All @@ -110,4 +123,3 @@ public void testCharacters() throws Exception
//@checkstyle-enable-check : IllegalTokenText
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -1942,13 +1942,11 @@ public Resource getResource(String path) throws MalformedURLException
if (_baseResource == null)
return null;

// Does the path go above the current scope?
path = URIUtil.canonicalPath(path);
if (path == null)
return null;

try
{
// addPath with accept non-canonical paths that don't go above the root,
// but will treat them as aliases. So unless allowed by an AliasChecker
// they will be rejected below.
Resource resource = _baseResource.addPath(path);

if (checkAlias(path, resource))
Expand Down Expand Up @@ -2299,6 +2297,10 @@ else if (path.charAt(0) != '/')
@Override
public URL getResource(String path) throws MalformedURLException
{
// This is an API call from the application which may have arbitrary non canonical paths passed
// Thus we canonicalize here, to avoid the enforcement of only canonical paths in
// ContextHandler.this.getResource(path).
path = URIUtil.canonicalPath(path);
gregw marked this conversation as resolved.
Show resolved Hide resolved
if (path == null)
return null;
Resource resource = ContextHandler.this.getResource(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ public Resource getResource(String path)
}
}
else if (_context != null)
{
r = _context.getResource(path);
}

if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
r = getStylesheet();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,12 @@ public void testBadUTF8FallsbackTo8859() throws Exception
Log.getLogger(HttpParser.class).info("badMessage: bad encoding expected ...");
String response;

response = connector.getResponse("GET /foo/bar%c0%00 HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n");
checkContains(response, 0, "HTTP/1.1 400");

response = connector.getResponse("GET /bad/utf8%c1 HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,16 +235,23 @@ public void testDoesNotExistResource() throws Exception
@Test
public void testAlias() throws Exception
{
Resource resource = context.getResource("/./index.html");
assertNotNull(resource);
assertFalse(resource.isAlias());
String path = "/./index.html";
Resource resource = context.getResource(path);
assertNull(resource);
URL resourceURL = context.getServletContext().getResource(path);
assertFalse(resourceURL.getPath().contains("/./"));

resource = context.getResource("/down/../index.html");
assertNotNull(resource);
assertFalse(resource.isAlias());
path = "/down/../index.html";
resource = context.getResource(path);
assertNull(resource);
resourceURL = context.getServletContext().getResource(path);
assertFalse(resourceURL.getPath().contains("/../"));

resource = context.getResource("//index.html");
path = "//index.html";
resource = context.getResource(path);
assertNull(resource);
resourceURL = context.getServletContext().getResource(path);
assertNull(resourceURL);
}

@ParameterizedTest
Expand Down
Loading