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:
parent
760257a06f
commit
d73252a28c
|
@ -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>
|
||||
|
|
|
@ -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&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&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&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(" ");
|
||||
buf.append("</a></td>");
|
||||
buf.append(" </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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue