Jetty 12 : `ResourceListing` produces XHTML (and is validated in test cases) (#8471)

* Produce XHTML output with tests that validate the XHTML.
* Adding ResourceListingTest and ensuring ResourceListing output is well formed.
* Introduce non-directory entry in ResourceListing test for ResourceCollection
This commit is contained in:
Joakim Erdfelt 2022-09-27 11:42:05 -05:00 committed by GitHub
parent 760257a06f
commit d73252a28c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 563 additions and 16 deletions

View File

@ -56,6 +56,11 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-xhtml-schemas</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http-tools</artifactId>

View File

@ -44,7 +44,7 @@ public class ResourceListing
public static final Logger LOG = LoggerFactory.getLogger(ResourceListing.class);
/**
* Convert the Resource directory into an HTML directory listing.
* Convert the Resource directory into an XHTML directory listing.
*
* @param resource the resource to build the listing from
* @param base The base URL
@ -52,7 +52,7 @@ public class ResourceListing
* @param query query params
* @return the HTML as String
*/
public static String getAsHTML(Resource resource, String base, boolean parent, String query)
public static String getAsXHTML(Resource resource, String base, boolean parent, String query)
{
// This method doesn't check aliases, so it is OK to canonicalize here.
base = URIUtil.normalizePath(base);
@ -105,13 +105,15 @@ public class ResourceListing
StringBuilder buf = new StringBuilder(4096);
// Doctype Declaration (HTML5)
buf.append("<!DOCTYPE html>\n");
buf.append("<html lang=\"en\">\n");
// Doctype Declaration + XHTML
buf.append("""
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
""");
// HTML Header
buf.append("<head>\n");
buf.append("<meta charset=\"utf-8\">\n");
buf.append("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />\n");
buf.append("<title>");
buf.append(title);
@ -145,7 +147,7 @@ public class ResourceListing
}
}
buf.append("<tr><th class=\"name\"><a href=\"?C=N&O=").append(order).append("\">");
buf.append("<tr><th class=\"name\"><a href=\"?C=N&amp;O=").append(order).append("\">");
buf.append("Name").append(arrow);
buf.append("</a></th>");
@ -165,7 +167,7 @@ public class ResourceListing
}
}
buf.append("<th class=\"lastmodified\"><a href=\"?C=M&O=").append(order).append("\">");
buf.append("<th class=\"lastmodified\"><a href=\"?C=M&amp;O=").append(order).append("\">");
buf.append("Last Modified").append(arrow);
buf.append("</a></th>");
@ -184,7 +186,7 @@ public class ResourceListing
arrow = ARROW_DOWN;
}
}
buf.append("<th class=\"size\"><a href=\"?C=S&O=").append(order).append("\">");
buf.append("<th class=\"size\"><a href=\"?C=S&amp;O=").append(order).append("\">");
buf.append("Size").append(arrow);
buf.append("</a></th></tr>\n");
buf.append("</thead>\n");
@ -197,6 +199,7 @@ public class ResourceListing
{
// Name
buf.append("<tr><td class=\"name\"><a href=\"");
// TODO This produces an absolute link from the /context/<listing-dir> path, investigate if we can use relative links reliably now
buf.append(URIUtil.addPaths(encodedBase, "../"));
buf.append("\">Parent Directory</a></td>");
// Last Modified
@ -228,8 +231,7 @@ public class ResourceListing
buf.append(path);
buf.append("\">");
buf.append(deTag(name));
buf.append("&nbsp;");
buf.append("</a></td>");
buf.append("&nbsp;</a></td>");
// Last Modified
buf.append("<td class=\"lastmodified\">");
@ -262,11 +264,18 @@ public class ResourceListing
}
/**
* <p>
* Encode any characters that could break the URI string in an HREF.
* Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
* </p>
*
* <p>
* Such as:
* {@code <a href="/path/to;<script>Window.alert('XSS'+'%20'+'here');</script>">Link</a>}
* </p>
* <p>
* The above example would parse incorrectly on various browsers as the "<" or '"' characters
* would end the href attribute value string prematurely.
* </p>
*
* @param raw the raw text to encode.
* @return the defanged text.

View File

@ -559,7 +559,7 @@ public class ResourceService
}
String base = URIUtil.addEncodedPaths(request.getHttpURI().getPath(), URIUtil.SLASH);
String listing = ResourceListing.getAsHTML(httpContent.getResource(), base, pathInContext.length() > 1, request.getHttpURI().getQuery());
String listing = ResourceListing.getAsXHTML(httpContent.getResource(), base, pathInContext.length() > 1, request.getHttpURI().getQuery());
if (listing == null)
{
writeHttpError(request, response, callback, HttpStatus.FORBIDDEN_403);

View File

@ -0,0 +1,497 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
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.regex.Pattern;
import javax.xml.catalog.Catalog;
import javax.xml.catalog.CatalogManager;
import javax.xml.catalog.CatalogResolver;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.xhtml.CatalogXHTML;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class ResourceListingTest
{
@Test
public void testBasicResourceXHtmlListingRoot(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
FS.touch(root.resolve("entry1.txt"));
FS.touch(root.resolve("entry2.dat"));
Files.createDirectory(root.resolve("dirFoo"));
Files.createDirectory(root.resolve("dirBar"));
Files.createDirectory(root.resolve("dirZed"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(root);
String content = ResourceListing.getAsXHTML(resource, "/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/entry2.dat\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/dirBar/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/dirZed/\">"));
}
}
@Test
public void testBasicResourceXHtmlListingDeep(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
FS.touch(root.resolve("entry1.txt"));
FS.touch(root.resolve("entry2.dat"));
Files.createDirectory(root.resolve("dirFoo"));
Files.createDirectory(root.resolve("dirBar"));
Files.createDirectory(root.resolve("dirZed"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(root);
String content = ResourceListing.getAsXHTML(resource, "/deep/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/deep/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/deep/entry2.dat\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/deep/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/deep/dirBar/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/deep/dirZed/\">"));
}
}
@Test
public void testResourceCollectionXHtmlListingContext(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
Path docrootA = root.resolve("docrootA");
Files.createDirectory(docrootA);
FS.touch(docrootA.resolve("entry1.txt"));
FS.touch(docrootA.resolve("entry2.dat"));
FS.touch(docrootA.resolve("similar.txt"));
Files.createDirectory(docrootA.resolve("dirSame"));
Files.createDirectory(docrootA.resolve("dirFoo"));
Files.createDirectory(docrootA.resolve("dirBar"));
Path docrootB = root.resolve("docrootB");
Files.createDirectory(docrootB);
FS.touch(docrootB.resolve("entry3.png"));
FS.touch(docrootB.resolve("entry4.tar.gz"));
FS.touch(docrootB.resolve("similar.txt")); // same filename as in docrootA
Files.createDirectory(docrootB.resolve("dirSame")); // same directory name as in docrootA
Files.createDirectory(docrootB.resolve("dirCid"));
Files.createDirectory(docrootB.resolve("dirZed"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
List<URI> uriRootList = List.of(docrootA.toUri(), docrootB.toUri());
Resource resource = resourceFactory.newResource(uriRootList);
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/context/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/context/entry2.dat\">"));
assertThat(content, containsString("entry3.png"));
assertThat(content, containsString("<a href=\"/context/entry3.png\">"));
assertThat(content, containsString("entry4.tar.gz"));
assertThat(content, containsString("<a href=\"/context/entry4.tar.gz\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/context/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/context/dirBar/\">"));
assertThat(content, containsString("dirCid/"));
assertThat(content, containsString("<a href=\"/context/dirCid/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/context/dirZed/\">"));
int count;
// how many dirSame links do we have?
count = content.split(Pattern.quote("<a href=\"/context/dirSame/\">"), -1).length - 1;
assertThat(count, is(1));
// how many similar.txt do we have?
count = content.split(Pattern.quote("<a href=\"/context/similar.txt\">"), -1).length - 1;
assertThat(count, is(1));
}
}
@Test
public void testResourceCollectionMixedTypesXHtmlListingContext(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
Path docrootA = root.resolve("docrootA");
Files.createDirectory(docrootA);
FS.touch(docrootA.resolve("entry1.txt"));
FS.touch(docrootA.resolve("entry2.dat"));
Files.createDirectory(docrootA.resolve("dirFoo"));
Files.createDirectory(docrootA.resolve("dirBar"));
Path docrootB = root.resolve("docrootB");
Files.createDirectory(docrootB);
FS.touch(docrootB.resolve("entry3.png"));
FS.touch(docrootB.resolve("entry4.tar.gz"));
Files.createDirectory(docrootB.resolve("dirCid"));
Files.createDirectory(docrootB.resolve("dirZed"));
// Introduce a non-directory entry
Path docNonRootC = root.resolve("non-root.dat");
FS.touch(docNonRootC);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
// Collection consisting of file, dir, dir
List<URI> uriRootList = List.of(docNonRootC.toUri(), docrootA.toUri(), docrootB.toUri());
Resource resource = resourceFactory.newResource(uriRootList);
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/context/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/context/entry2.dat\">"));
assertThat(content, containsString("entry3.png"));
assertThat(content, containsString("<a href=\"/context/entry3.png\">"));
assertThat(content, containsString("entry4.tar.gz"));
assertThat(content, containsString("<a href=\"/context/entry4.tar.gz\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/context/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/context/dirBar/\">"));
assertThat(content, containsString("dirCid/"));
assertThat(content, containsString("<a href=\"/context/dirCid/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/context/dirZed/\">"));
assertThat(content, containsString("<a href=\"/context/non-root.dat\">"));
}
}
/**
* Test a ResourceCollection that is constructed by nested ResourceCollection.
*/
@Test
public void testResourceCollectionNestedXHtmlListingContext(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
Path docrootA = root.resolve("docrootA");
Files.createDirectory(docrootA);
FS.touch(docrootA.resolve("entry1.txt"));
FS.touch(docrootA.resolve("entry2.dat"));
Files.createDirectory(docrootA.resolve("dirFoo"));
Files.createDirectory(docrootA.resolve("dirBar"));
Path docrootB = root.resolve("docrootB");
Files.createDirectory(docrootB);
FS.touch(docrootB.resolve("entry3.png"));
FS.touch(docrootB.resolve("entry4.tar.gz"));
Files.createDirectory(docrootB.resolve("dirCid"));
Files.createDirectory(docrootB.resolve("dirZed"));
// Introduce a non-directory entry
Path docNonRootC = root.resolve("non-root.dat");
FS.touch(docNonRootC);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
// Collection consisting of inputs [file, dir]
Resource resourceCollectionA = resourceFactory.newResource(List.of(docNonRootC.toUri(), docrootB.toUri()));
// Basic resource, just a dir
Resource basicResource = resourceFactory.newResource(docrootA);
// New Collection consisting of inputs [collection, dir] - resulting in [file, dir, dir]
Resource resourceCollectionB = Resource.combine(resourceCollectionA, basicResource);
// Use collection in generating the output
String content = ResourceListing.getAsXHTML(resourceCollectionB, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/context/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/context/entry2.dat\">"));
assertThat(content, containsString("entry3.png"));
assertThat(content, containsString("<a href=\"/context/entry3.png\">"));
assertThat(content, containsString("entry4.tar.gz"));
assertThat(content, containsString("<a href=\"/context/entry4.tar.gz\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/context/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/context/dirBar/\">"));
assertThat(content, containsString("dirCid/"));
assertThat(content, containsString("<a href=\"/context/dirCid/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/context/dirZed/\">"));
assertThat(content, containsString("<a href=\"/context/non-root.dat\">"));
}
}
/**
* A regression on windows allowed the directory listing show
* the fully qualified paths within the directory listing.
* This test ensures that this behavior will not arise again.
*/
@Test
public void testListingFilenamesOnly(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
FS.ensureDirExists(docRoot);
Path one = docRoot.resolve("one");
FS.ensureDirExists(one);
Path deep = one.resolve("deep");
FS.ensureDirExists(deep);
FS.touch(deep.resolve("foo"));
FS.ensureDirExists(docRoot.resolve("two"));
FS.ensureDirExists(docRoot.resolve("three"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resourceBase = resourceFactory.newResource(docRoot);
Resource resource = resourceBase.resolve("one/deep/");
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("/foo"));
String resBasePath = docRoot.toAbsolutePath().toString();
assertThat(content, not(containsString(resBasePath)));
}
}
@Test
public void testListingProperUrlEncoding(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
Path wackyDir = docRoot.resolve("dir;"); // this should not be double-encoded.
FS.ensureDirExists(wackyDir);
FS.ensureDirExists(wackyDir.resolve("four"));
FS.ensureDirExists(wackyDir.resolve("five"));
FS.ensureDirExists(wackyDir.resolve("six"));
/* At this point we have the following
* testListingProperUrlEncoding/
* `-- docroot
* `-- dir;
* |-- five
* |-- four
* `-- six
*/
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resourceBase = resourceFactory.newResource(docRoot);
// Resolve directory
Resource resource = resourceBase.resolve("dir%3B");
// Context
String content = ResourceListing.getAsXHTML(resource, "/context/dir%3B/", false, null);
assertTrue(isValidXHtml(content));
// Should not see double-encoded ";"
// First encoding: ";" -> "%3B"
// Second encoding: "%3B" -> "%253B" (BAD!)
assertThat(content, not(containsString("%253B")));
assertThat(content, containsString("/dir%3B/"));
assertThat(content, containsString("/dir%3B/four/"));
assertThat(content, containsString("/dir%3B/five/"));
assertThat(content, containsString("/dir%3B/six/"));
}
}
@Test
public void testListingWithQuestionMarks(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
FS.ensureDirExists(docRoot.resolve("one"));
FS.ensureDirExists(docRoot.resolve("two"));
FS.ensureDirExists(docRoot.resolve("three"));
// Creating dir 'f??r' (Might not work in Windows)
assumeMkDirSupported(docRoot, "f??r");
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(docRoot);
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("f??r"));
}
}
@Test
public void testListingEncoding(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
Path one = docRoot.resolve("one");
FS.ensureDirExists(one);
// example of content on disk that could cause problems when taken to the HTML space.
Path alert = one.resolve("onmouseclick='alert(oops)'");
FS.touch(alert);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resourceBase = resourceFactory.newResource(docRoot);
Resource resource = resourceBase.resolve("one");
String content = ResourceListing.getAsXHTML(resource, "/context/one", false, null);
assertTrue(isValidXHtml(content));
// Entry should be properly encoded
assertThat(content, containsString("<a href=\"/context/one/onmouseclick=%27alert(oops)%27\">"));
}
}
private static boolean isValidXHtml(String content)
{
// we expect that our generated output conforms to text/xhtml is well-formed
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))
{
Catalog catalog = CatalogXHTML.getCatalog();
CatalogResolver resolver = CatalogManager.catalogResolver(catalog);
DocumentBuilderFactory xmlDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
xmlDocumentBuilderFactory.setValidating(true);
DocumentBuilder db = xmlDocumentBuilderFactory.newDocumentBuilder();
db.setEntityResolver(resolver);
List<SAXParseException> errors = new ArrayList<>();
db.setErrorHandler(new ErrorHandler()
{
@Override
public void warning(SAXParseException exception)
{
exception.printStackTrace();
}
@Override
public void error(SAXParseException exception)
{
errors.add(exception);
}
@Override
public void fatalError(SAXParseException exception)
{
errors.add(exception);
}
});
// We consider this content to be XML well-formed if these 2 lines do not throw an Exception
Document doc = db.parse(inputStream);
doc.getDocumentElement().normalize();
if (errors.size() > 0)
{
IOException ioException = new IOException("Failed to validate XHTML");
for (SAXException saxException : errors)
{
ioException.addSuppressed(saxException);
}
fail(ioException);
}
return true; // it's well-formed
}
catch (IOException | ParserConfigurationException | SAXException e)
{
e.printStackTrace(System.err);
return false; // XHTML has got issues
}
}
/**
* Attempt to create the directory, skip testcase if not supported on OS.
*/
private static Path assumeMkDirSupported(Path path, String subpath)
{
Path ret = null;
try
{
ret = path.resolve(subpath);
if (Files.exists(ret))
return ret;
Files.createDirectories(ret);
}
catch (InvalidPathException | IOException ignore)
{
// ignore
}
assumeTrue(ret != null, "Directory creation not supported on OS: " + path + File.separator + subpath);
assumeTrue(Files.exists(ret), "Directory creation not supported on OS: " + ret);
return ret;
}
}

View File

@ -42,6 +42,7 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
@ -2626,6 +2627,11 @@ public class ResourceHandlerTest
assertThat(response.get(LOCATION), endsWith("/context/"));
}
/**
* Tests to attempt to break out of the Context restrictions by
* abusing encoding (or lack thereof), listing output,
* welcome file behaviors, and more.
*/
@ParameterizedTest
@MethodSource("contextBreakoutScenarios")
public void testListingContextBreakout(ResourceHandlerTest.Scenario scenario) throws Exception
@ -2784,9 +2790,27 @@ public class ResourceHandlerTest
assertThat(body, containsString("f??r"));
}
/**
* <p>
* Tests to ensure that when requesting a legit directory listing, you
* cannot arbitrarily include XSS in the output via careful manipulation
* of the request path.
* </p>
* <p>
* This is mainly a test of how the raw request details evolve over time, and
* migrate through the ResourceHandler before it hits the
* ResourceListing.getAsXHTML for output production
* </p>
*/
@Test
public void testListingXSS() throws Exception
{
// Allow unsafe URI requests for this test case specifically
// The requests below abuse the path-param features of URI, and the default UriCompliance mode
// will prevent the use those requests as a 400 Bad Request: Ambiguous URI empty segment
HttpConfiguration httpConfiguration = _local.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration();
httpConfiguration.setUriCompliance(UriCompliance.UNSAFE);
/* create some content in the docroot */
Path one = docRoot.resolve("one");
FS.ensureDirExists(one);
@ -2798,7 +2822,9 @@ public class ResourceHandlerTest
/*
* Intentionally bad request URI. Sending a non-encoded URI with typically
* encoded characters '<', '>', and '"'.
* encoded characters '<', '>', and '"', using the path-param feature of the
* URI spec to still produce a listing. This path-param value should not make it
* down to the ResourceListing.getAsXHTML() method.
*/
String req1 = """
GET /context/;<script>window.alert("hi");</script> HTTP/1.1\r

View File

@ -285,7 +285,10 @@ public class ResourceCollection extends Resource
List<Resource> result = new ArrayList<>();
for (Resource r : _resources)
{
result.addAll(r.list());
if (r.isDirectory())
result.addAll(r.list());
else
result.add(r);
}
return result;
}

View File

@ -636,7 +636,7 @@ public class ResourceService
byte[] data = null;
String base = URIUtil.addEncodedPaths(request.getRequestURI(), URIUtil.SLASH);
String dir = ResourceListing.getAsHTML(resource, base, pathInContext.length() > 1, request.getQueryString());
String dir = ResourceListing.getAsXHTML(resource, base, pathInContext.length() > 1, request.getQueryString());
if (dir == null)
{
response.sendError(HttpServletResponse.SC_FORBIDDEN,

View File

@ -61,6 +61,7 @@
<jetty-quiche-native.version>0.12.0</jetty-quiche-native.version>
<jetty-test-policy.version>1.2</jetty-test-policy.version>
<jetty.test.version>5.9</jetty.test.version>
<jetty.xhtml.schemas-version>1.1</jetty.xhtml.schemas-version>
<jmh.version>1.34</jmh.version>
<jna.version>5.10.0</jna.version>
<jnr-constants.version>0.10.3</jnr-constants.version>
@ -1480,6 +1481,12 @@
<artifactId>jetty-test-helper</artifactId>
<version>${jetty.test.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-xhtml-schemas</artifactId>
<version>${jetty.xhtml.schemas-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>