Issue #4033 Percent Encoded Bad Requests (#4272)

* Modernizing testcase

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>

* Issue #4033 Percent Encoded Bad Requests

Added test to demonstrate bad percent encoded request

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Issue #4033 - adding sanity test for percent paths and checkAlias()

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>

* Eliminating 9.3.0.RC0 dependency

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>

* Issue #4033 - More tests for Resource checkAlias() behavior

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>

* Issue #4033 - Splitting badDecodePath

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>

* Issue #4033 - More badDecodePath tests

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>

* Issue #4033 Percent Encoded Bad Requests

reverted decodePathBehaviour

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* testing pull request building

* Issue #4033

updates after review

Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Greg Wilkins 2019-11-11 12:01:26 +11:00 committed by GitHub
parent c336616c96
commit ee0f9fc1d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 216 additions and 130 deletions

View File

@ -824,12 +824,11 @@ public class HttpConnectionTest
@Test
public void testBadURIencoding() throws Exception
{
// The URI is being leniently decoded, leaving the "%x" alone
String response = connector.getResponse("GET /bad/encoding%x HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n");
checkContains(response, 0, "HTTP/1.1 200");
checkContains(response, 0, "HTTP/1.1 400");
}
@Test

View File

@ -251,6 +251,24 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
}
}
@Test
public void testBadURI() throws Exception
{
configureServer(new HelloWorldHandler());
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
os.write("GET /%xx HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// Read the response.
String response = readResponse(client);
assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
}
}
@Test
public void testExceptionThrownInHandlerLoop() throws Exception
{

View File

@ -470,70 +470,25 @@ public class URIUtil
builder = new Utf8StringBuilder(path.length());
builder.append(path, offset, i - offset);
}
// lenient percent decoding
if (i >= end)
if ((i + 2) < end)
{
// [LENIENT] a percent sign at end of string.
builder.append('%');
i = end;
}
else if (end > (i + 1))
{
char type = path.charAt(i + 1);
if (type == 'u')
char u = path.charAt(i + 1);
if (u == 'u')
{
// We have a possible (deprecated) microsoft unicode code point "%u####"
// - not recommended to use as it's limited to 2 bytes.
if ((i + 5) >= end)
{
// [LENIENT] we have a partial "%u####" at the end of a string.
builder.append(path, i, (end - i));
i = end;
}
else
{
// this seems wrong, as we are casting to a char, but that's the known
// limitation of this deprecated encoding (only 2 bytes allowed)
if (StringUtil.isHex(path, i + 2, 4))
{
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5;
}
else
{
// [LENIENT] copy the "%u" as-is.
builder.append(path, i, 2);
i += 1;
}
}
}
else if (end > (i + 2))
{
// we have a possible "%##" encoding
if (StringUtil.isHex(path, i + 1, 2))
{
builder.append((byte)TypeUtil.parseInt(path, i + 1, 2, 16));
i += 2;
}
else
{
builder.append(path, i, 3);
i += 2;
}
// TODO remove %u support in jetty-10
// this is wrong. This is a codepoint not a char
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5;
}
else
{
// [LENIENT] incomplete "%##" sequence at end of string
builder.append(path, i, (end - i));
i = end;
builder.append((byte)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2)))));
i += 2;
}
}
else
{
// [LENIENT] the "%" at the end of the string
builder.append(path, i, (end - i));
i = end;
throw new IllegalArgumentException("Bad URI % encoding");
}
break;
@ -571,10 +526,18 @@ public class URIUtil
}
catch (NotUtf8Exception e)
{
LOG.warn(path.substring(offset, offset + length) + " " + e);
LOG.debug(e);
LOG.debug(path.substring(offset, offset + length) + " " + e);
return decodeISO88591Path(path, offset, length);
}
catch (IllegalArgumentException e)
{
throw e;
}
catch (Exception e)
{
throw new IllegalArgumentException("cannot decode URI", e);
}
}
/* Decode a URI path and strip parameters of ISO-8859-1 path
@ -599,13 +562,14 @@ public class URIUtil
char u = path.charAt(i + 1);
if (u == 'u')
{
// TODO this is wrong. This is a codepoint not a char
// TODO remove %u encoding support in jetty-10
// This is wrong. This is a codepoint not a char
builder.append((char)(0xffff & TypeUtil.parseInt(path, i + 2, 4, 16)));
i += 5;
}
else
{
builder.append((byte)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2)))));
builder.append((char)(0xff & (TypeUtil.convertHexDigit(u) * 16 + TypeUtil.convertHexDigit(path.charAt(i + 2)))));
i += 2;
}
}

View File

@ -33,6 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
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;
/**
@ -94,18 +95,6 @@ public class URIUtilTest
// Deprecated Microsoft Percent-U encoding
arguments.add(Arguments.of("abc%u3040", "abc\u3040"));
// Lenient decode
arguments.add(Arguments.of("abc%xyz", "abc%xyz")); // not a "%##"
arguments.add(Arguments.of("abc%", "abc%")); // percent at end of string
arguments.add(Arguments.of("abc%A", "abc%A")); // incomplete "%##" at end of string
arguments.add(Arguments.of("abc%uvwxyz", "abc%uvwxyz")); // not a valid "%u####"
arguments.add(Arguments.of("abc%uEFGHIJ", "abc%uEFGHIJ")); // not a valid "%u####"
arguments.add(Arguments.of("abc%uABC", "abc%uABC")); // incomplete "%u####"
arguments.add(Arguments.of("abc%uAB", "abc%uAB")); // incomplete "%u####"
arguments.add(Arguments.of("abc%uA", "abc%uA")); // incomplete "%u####"
arguments.add(Arguments.of("abc%u", "abc%u")); // incomplete "%u####"
return arguments.stream();
}
@ -117,6 +106,60 @@ public class URIUtilTest
assertEquals(expectedPath, path);
}
public static Stream<Arguments> decodeBadPathSource()
{
List<Arguments> arguments = new ArrayList<>();
// Test for null character (real world ugly test case)
// TODO is this a bad decoding or a bad URI ?
// arguments.add(Arguments.of("/%00/"));
// Deprecated Microsoft Percent-U encoding
// TODO still supported for now ?
// arguments.add(Arguments.of("abc%u3040"));
// Bad %## encoding
arguments.add(Arguments.of("abc%xyz"));
// Incomplete %## encoding
arguments.add(Arguments.of("abc%"));
arguments.add(Arguments.of("abc%A"));
// Invalid microsoft %u#### encoding
arguments.add(Arguments.of("abc%uvwxyz"));
arguments.add(Arguments.of("abc%uEFGHIJ"));
// Incomplete microsoft %u#### encoding
arguments.add(Arguments.of("abc%uABC"));
arguments.add(Arguments.of("abc%uAB"));
arguments.add(Arguments.of("abc%uA"));
arguments.add(Arguments.of("abc%u"));
// Invalid UTF-8 and ISO8859-1
// TODO currently ISO8859 is too forgiving to detect these
/*
arguments.add(Arguments.of("abc%C3%28")); // invalid 2 octext sequence
arguments.add(Arguments.of("abc%A0%A1")); // invalid 2 octext sequence
arguments.add(Arguments.of("abc%e2%28%a1")); // invalid 3 octext sequence
arguments.add(Arguments.of("abc%e2%82%28")); // invalid 3 octext sequence
arguments.add(Arguments.of("abc%f0%28%8c%bc")); // invalid 4 octext sequence
arguments.add(Arguments.of("abc%f0%90%28%bc")); // invalid 4 octext sequence
arguments.add(Arguments.of("abc%f0%28%8c%28")); // invalid 4 octext sequence
arguments.add(Arguments.of("abc%f8%a1%a1%a1%a1")); // valid sequence, but not unicode
arguments.add(Arguments.of("abc%fc%a1%a1%a1%a1%a1")); // valid sequence, but not unicode
arguments.add(Arguments.of("abc%f8%a1%a1%a1")); // incomplete sequence
*/
return arguments.stream();
}
@ParameterizedTest(name = "[{index}] {0}")
@MethodSource("decodeBadPathSource")
public void testBadDecodePath(String encodedPath)
{
assertThrows(IllegalArgumentException.class, () -> URIUtil.decodePath(encodedPath));
}
@Test
public void testDecodePathSubstring()
{

View File

@ -18,93 +18,155 @@
package org.eclipse.jetty.util.resource;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.log.Log;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(WorkDirExtension.class)
public class ResourceAliasTest
{
static File __dir;
public WorkDir workDir;
@BeforeAll
public static void beforeClass()
public static Stream<Function<Path, Resource>> resourceTypes()
{
__dir = MavenTestingUtils.getTargetTestingDir("RAT");
List<Function<Path, Resource>> types = new ArrayList<>();
types.add((path) -> new PathResource(path));
types.add((path) -> new FileResource(path.toFile()));
return types.stream();
}
@BeforeEach
public void before()
@ParameterizedTest
@MethodSource("resourceTypes")
public void testPercentPaths(Function<Path, Resource> resourceType) throws IOException
{
FS.ensureDirExists(__dir);
FS.ensureEmpty(__dir);
Path baseDir = workDir.getEmptyPathDir();
Path foo = baseDir.resolve("%foo");
Files.createDirectories(foo);
Path bar = foo.resolve("bar%");
Files.createDirectories(bar);
Path text = bar.resolve("test.txt");
FS.touch(text);
// At this point we have a path .../%foo/bar%/test.txt present on the filesystem.
// This would also apply for paths found in JAR files (like META-INF/resources/%foo/bar%/test.txt)
assertTrue(Files.exists(text));
Resource baseResource = resourceType.apply(baseDir);
assertTrue(baseResource.exists(), "baseResource exists");
Resource fooResource = baseResource.addPath("%foo");
assertTrue(fooResource.exists(), "fooResource exists");
assertTrue(fooResource.isDirectory(), "fooResource isDir");
if (fooResource instanceof FileResource)
assertTrue(fooResource.isAlias(), "fooResource isAlias");
else
assertFalse(fooResource.isAlias(), "fooResource isAlias");
Resource barResource = fooResource.addPath("bar%");
assertTrue(barResource.exists(), "barResource exists");
assertTrue(barResource.isDirectory(), "barResource isDir");
if (fooResource instanceof FileResource)
assertTrue(barResource.isAlias(), "barResource isAlias");
else
assertFalse(barResource.isAlias(), "barResource isAlias");
Resource textResource = barResource.addPath("test.txt");
assertTrue(textResource.exists(), "textResource exists");
assertFalse(textResource.isDirectory(), "textResource isDir");
}
@Test
public void testNullCharEndingFilename() throws Exception
{
File file = new File(__dir, "test.txt");
assertFalse(file.exists());
assertTrue(file.createNewFile());
assertTrue(file.exists());
Path baseDir = workDir.getEmptyPathDir();
File file0 = new File(__dir, "test.txt\0");
if (!file0.exists())
return; // this file system does not suffer this problem
assertTrue(file0.exists()); // This is an alias!
Resource dir = Resource.newResource(__dir);
// Test not alias paths
Resource resource = Resource.newResource(file);
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = Resource.newResource(file.getAbsoluteFile());
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = Resource.newResource(file.toURI());
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = Resource.newResource(file.toURI().toString());
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = dir.addPath("test.txt");
assertTrue(resource.exists());
assertNull(resource.getAlias());
// Test alias paths
resource = Resource.newResource(file0);
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
resource = Resource.newResource(file0.getAbsoluteFile());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
resource = Resource.newResource(file0.toURI());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
resource = Resource.newResource(file0.toURI().toString());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
Path file = baseDir.resolve("test.txt");
FS.touch(file);
try
{
resource = dir.addPath("test.txt\0");
Path file0 = baseDir.resolve("test.txt\0");
if (!Files.exists(file0))
return; // this file system does get tricked by ending filenames
assertThat(file0 + " exists", Files.exists(file0), is(true)); // This is an alias!
Resource dir = Resource.newResource(baseDir);
// Test not alias paths
Resource resource = Resource.newResource(file);
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = Resource.newResource(file.toAbsolutePath());
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = Resource.newResource(file.toUri());
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = Resource.newResource(file.toUri().toString());
assertTrue(resource.exists());
assertNull(resource.getAlias());
resource = dir.addPath("test.txt");
assertTrue(resource.exists());
assertNull(resource.getAlias());
// Test alias paths
resource = Resource.newResource(file0);
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
resource = Resource.newResource(file0.toAbsolutePath());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
resource = Resource.newResource(file0.toUri());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
resource = Resource.newResource(file0.toUri().toString());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
try
{
resource = dir.addPath("test.txt\0");
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
}
catch (MalformedURLException e)
{
assertTrue(true);
}
}
catch (MalformedURLException e)
catch (InvalidPathException e)
{
assertTrue(true);
// this file system does allow null char ending filenames
Log.getRootLogger().ignore(e);
}
}
}