CVE-2021-32643: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Http4s is a Scala interface for HTTP services. `StaticFile.fromUrl` can leak the presence of a directory on a server when the `URL` scheme is not `file://`, and the URL points to a fetchable resource under its scheme and authority. The function returns `F[None]`, indicating no resource, if `url.getFile` is a directory, without first checking the scheme or authority of the URL. If a URL connection to the scheme and URL would return a stream, and the path in the URL exists as a directory on the server, the presence of the directory on the server could be inferred from the 404 response. The contents and other metadata about the directory are not exposed. This affects http4s versions: 0.21.7 through 0.21.23, 0.22.0-M1 through 0.22.0-M8, 0.23.0-M1, and 1.0.0-M1 through 1.0.0-M22. The [patch](https://github.com/http4s/http4s/commit/52e1890665410b4385e37b96bc49c5e3c708e4e9) is available in the following versions: v0.21.24, v0.22.0-M9, v0.23.0-M2, v1.0.0-M23. As a workaround users can avoid calling `StaticFile.fromUrl` with non-file URLs.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 5.8
Published: 2021-05-27T18:15Z
Repository: https://github.com/http4s/http4s
Language: Scala
Files changed: core/src/main/scala/org/http4s/StaticFile.scala, tests/src/test/scala/org/http4s/StaticFileSuite.scala
Commit date: 2021-05-26 00:38:03 -0400
Commit message: Fix directory leak in StaticFile.fromUrl

Diff: diff --git a/core/src/main/scala/org/http4s/StaticFile.scala b/core/src/main/scala/org/http4s/StaticFile.scala
index b0cf260726..26f35ea3de 100644
--- a/core/src/main/scala/org/http4s/StaticFile.scala
+++ b/core/src/main/scala/org/http4s/StaticFile.scala
@@ -78,11 +78,11 @@ object StaticFile {
       F: Sync[F],
       cs: ContextShift[F]): OptionT[F, Response[F]] = {
     val fileUrl = url.getFile()
     val file = new File(fileUrl)
     OptionT.apply(F.defer {
-      if (file.isDirectory())
+      if (url.getProtocol === "file" && file.isDirectory)
         F.pure(None)
       else {
         val urlConn = url.openConnection
         val lastmod = HttpDate.fromEpochSecond(urlConn.getLastModified / 1000).toOption
         val ifModifiedSince = req.flatMap(_.headers.get(`If-Modified-Since`))
diff --git a/tests/src/test/scala/org/http4s/StaticFileSuite.scala b/tests/src/test/scala/org/http4s/StaticFileSuite.scala
index 579577ae72..4b1c9ceecd 100644
--- a/tests/src/test/scala/org/http4s/StaticFileSuite.scala
+++ b/tests/src/test/scala/org/http4s/StaticFileSuite.scala
@@ -275,18 +275,32 @@ class StaticFileSuite extends Http4sSuite {
         .value
         .map(_.flatMap(_.contentLength))
     len.assertEquals(Some(24005L))
   }
 
-  test("return none from a URL that is a directory") {
+  test("return none from a file URL that is a directory") {
     // val url = getClass.getResource("/foo")
     StaticFile
       .fromURL[IO](getClass.getResource("/foo"), testBlocker)
       .value
       .assertEquals(None)
   }
 
+  test("not return none from an HTTP URL whose path is a directory") {
+    // We need a universal directory that also exists as a resource on
+    // a server.  Creating a temp directory would be better, but then
+    // we need an HTTP server that responds to a wildcard path.
+    //
+    // Or we can be lazy and just use `/`.
+    assume(new File("/").isDirectory, "/ is not a directory")
+    StaticFile
+      .fromURL[IO](new URL("https://github.com//"), testBlocker)
+      .value
+      .map(_.fold(Status.NotFound)(_.status))
+      .assertEquals(Status.Ok)
+  }
+
   test("return none from a URL that points to a resource that does not exist") {
     StaticFile
       .fromURL[IO](new URL("https://github.com/http4s/http4s/fooz"), testBlocker)
       .value
       .assertEquals(None)

Vulnerable code:       if (file.isDirectory())
  test("return none from a URL that is a directory") {
Fixed code:       if (url.getProtocol === "file" && file.isDirectory)
  test("return none from a file URL that is a directory") {
  test("not return none from an HTTP URL whose path is a directory") {
    // We need a universal directory that also exists as a resource on
    // a server.  Creating a temp directory would be better, but then
    // we need an HTTP server that responds to a wildcard path.
    //
    // Or we can be lazy and just use `/`.
    assume(new File("/").isDirectory, "/ is not a directory")
    StaticFile
      .fromURL[IO](new URL("https://github.com//"), testBlocker)
      .value
      .map(_.fold(Status.NotFound)(_.status))
      .assertEquals(Status.Ok)
  }