diff --git a/cli/Valet/Server.php b/cli/Valet/Server.php
index edccf06..224037d 100644
--- a/cli/Valet/Server.php
+++ b/cli/Valet/Server.php
@@ -75,25 +75,42 @@ public static function showDirectoryListing(string $valetSitePath, string $uri)
}
// Sort directories at the top
- $paths = glob("$directory/*");
+ $paths = glob("$directory/*") ?: [];
usort($paths, function ($a, $b) {
return (is_dir($a) == is_dir($b)) ? strnatcasecmp($a, $b) : (is_dir($a) ? -1 : 1);
});
- // Output the HTML for the directory listing
- echo "
Index of $uri
";
- echo '
';
- echo implode(
- '
'.PHP_EOL,
- array_map(function ($path) use ($uri, $isRoot) {
- $file = basename($path);
- return ($isRoot) ? "/$file" : "$uri/$file/";
- }, $paths)
- );
+ echo static::directoryListingHtml($uri, $paths, $isRoot);
exit;
}
+ /**
+ * Build the HTML for a directory listing.
+ *
+ * The request URI and the file names are HTML-escaped: both are
+ * attacker-influenceable (the URI directly, a file name via a crafted
+ * directory entry), so echoing them raw would allow reflected/stored XSS.
+ *
+ * @param array $paths
+ */
+ public static function directoryListingHtml(string $uri, array $paths, bool $isRoot): string
+ {
+ $uriEscaped = htmlspecialchars($uri, ENT_QUOTES, 'UTF-8');
+
+ $links = array_map(function ($path) use ($uriEscaped, $isRoot) {
+ $fileEscaped = htmlspecialchars(basename($path), ENT_QUOTES, 'UTF-8');
+
+ return $isRoot
+ ? "/$fileEscaped"
+ : "$uriEscaped/$fileEscaped/";
+ }, $paths);
+
+ return 'Index of '.$uriEscaped.'
'
+ .'
'
+ .implode('
'.PHP_EOL, $links);
+ }
+
/**
* Return whether a given host (from $_SERVER['HTTP_HOST']) is an IP address.
*/
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index f292770..3651df3 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -460,21 +460,11 @@ parameters:
count: 1
path: cli/Valet/Server.php
- -
- message: "#^Parameter \\#1 \\$array of function usort expects TArray of array\\, array\\\\|false given\\.$#"
- count: 1
- path: cli/Valet/Server.php
-
-
message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|null given\\.$#"
count: 1
path: cli/Valet/Server.php
- -
- message: "#^Parameter \\#2 \\$array of function array_map expects array, array\\\\|false given\\.$#"
- count: 1
- path: cli/Valet/Server.php
-
-
message: "#^Parameter \\#2 \\$string of function explode expects string, string\\|null given\\.$#"
count: 1
diff --git a/tests/Unit/ServerTest.php b/tests/Unit/ServerTest.php
index 1e241dc..78fbeaa 100644
--- a/tests/Unit/ServerTest.php
+++ b/tests/Unit/ServerTest.php
@@ -121,4 +121,37 @@ public function it_returns_null_when_the_ip_address_uri_has_no_matching_tld(): v
$this->assertNull(Server::valetSiteFromIpAddressUri('my-site.dev/auth', 'test'));
$this->assertNull(Server::valetSiteFromIpAddressUri('', 'test'));
}
+
+ /**
+ * @test
+ */
+ public function it_escapes_the_uri_in_the_directory_listing(): void
+ {
+ $html = Server::directoryListingHtml('/', [], false);
+
+ $this->assertStringNotContainsString('