Skip to content
Open
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
final class WebviewAssetServerTest {

private static final int HTTP_OK = 200;
private static final int HTTP_BAD_REQUEST = 400;
private static final int HTTP_FORBIDDEN = 403;
private static final int HTTP_NOT_FOUND = 404;

@Test
Expand Down Expand Up @@ -93,6 +95,79 @@ void returnsNotFoundForMissingAsset(@TempDir final Path tempDir) throws Exceptio
}
}

@Test
void rejectsPathTraversalWithForbidden(@TempDir final Path tempDir) throws Exception {
// Create a sibling directory with a secret file that should never be reachable.
Path sibling = tempDir.resolveSibling("secret");
Files.createDirectories(sibling);
Files.writeString(sibling.resolve("credentials.txt"), "TOP_SECRET");

// The base dir has a legitimate asset so the server starts normally.
Path baseDir = tempDir.resolve("webview");
Files.createDirectories(baseDir);
Files.writeString(baseDir.resolve("index.html"), "<html></html>");

WebviewAssetServer server = new WebviewAssetServer();
try {
assertTrue(server.resolve(baseDir.toString()));

// Attempt to traverse out of baseDir to the sibling secret file.
// Jetty 12 rejects path traversal sequences at the framework level (400) before
// the handler's explicit startsWith guard (403) is reached. Both layers prevent escape.
HttpResponse<String> response = get(server, "../secret/credentials.txt");

assertTrue(response.statusCode() == HTTP_BAD_REQUEST || response.statusCode() == HTTP_FORBIDDEN,
"Path traversal must be rejected with 400 or 403, got: " + response.statusCode());
// Verify the secret content was NOT leaked in the response body.
assertTrue(!response.body().contains("TOP_SECRET"),
"Path traversal must not leak file contents outside the base directory");
} finally {
server.stop();
// Clean up sibling directory (not managed by @TempDir).
Files.deleteIfExists(sibling.resolve("credentials.txt"));
Files.deleteIfExists(sibling);
}
}

@Test
void rejectsInvalidPathWithBadRequest(@TempDir final Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("index.html"), "<html></html>");

WebviewAssetServer server = new WebviewAssetServer();
try {
assertTrue(server.resolve(tempDir.toString()));

// A percent-encoded null byte (%00) is valid in a URI but triggers InvalidPathException
// when decoded and resolved against the filesystem.
HttpResponse<String> response = get(server, "assets%00evil.js");

assertEquals(HTTP_BAD_REQUEST, response.statusCode());
} finally {
server.stop();
}
}

@Test
void servesDeeplyNestedAssetWithoutOverBlocking(@TempDir final Path tempDir) throws Exception {
// Ensure the traversal guard does not reject legitimate deeply-nested paths.
Path deep = tempDir.resolve("js").resolve("vendor").resolve("lib.js");
Files.createDirectories(deep.getParent());
String expectedContents = "var lib = 1;";
Files.writeString(deep, expectedContents);

WebviewAssetServer server = new WebviewAssetServer();
try {
assertTrue(server.resolve(tempDir.toString()));

HttpResponse<String> response = get(server, "js/vendor/lib.js");

assertEquals(HTTP_OK, response.statusCode());
assertEquals(expectedContents, response.body());
} finally {
server.stop();
}
}

private static HttpResponse<String> get(final WebviewAssetServer server, final String assetPath) throws Exception {
// Connect over 127.0.0.1 to satisfy the server's virtual host restriction, regardless of the host returned by getUri().
int port = URI.create(server.getUri()).getPort();
Expand Down
Loading