Skip to content

Commit

Permalink
Merge pull request from GHSA-66q9-f7ff-mmx6
Browse files Browse the repository at this point in the history
Fix GHSA-66q9-f7ff-mmx6 for 0.20.x
  • Loading branch information
rossabaker committed Mar 25, 2020
2 parents c253f05 + 4e23069 commit 752b3f6
Show file tree
Hide file tree
Showing 15 changed files with 458 additions and 73 deletions.
8 changes: 8 additions & 0 deletions build.sbt
Expand Up @@ -136,6 +136,14 @@ lazy val server = libraryProject("server")
.settings(
description := "Base library for building http4s servers"
)
.settings(BuildInfoPlugin.buildInfoScopedSettings(Test))
.settings(BuildInfoPlugin.buildInfoDefaultSettings)
.settings(
buildInfoKeys := Seq[BuildInfoKey](
resourceDirectory in Test,
),
buildInfoPackage := "org.http4s.server.test"
)
.dependsOn(core, testing % "test->test", theDsl % "test->compile")

lazy val prometheusMetrics = libraryProject("prometheus-metrics")
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
@@ -1 +1 @@
sbt.version=1.3.7
sbt.version=1.3.8
2 changes: 1 addition & 1 deletion project/plugins.sbt
Expand Up @@ -13,7 +13,7 @@ addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.
addSbtPlugin("com.github.tkawachi" % "sbt-doctest" % "0.7.2")
addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.15")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.4.0")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.6.1")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.7.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.3.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.4.2")
Expand Down
Expand Up @@ -6,11 +6,19 @@ import cats.data._
import cats.effect._
import cats.implicits._
import java.io.File
import java.nio.file.{LinkOption, Paths}
import org.http4s.headers.Range.SubRange
import org.http4s.headers._
import org.http4s.server.middleware.TranslateUri
import org.log4s.getLogger
import scala.concurrent.ExecutionContext
import scala.util.control.NoStackTrace
import scala.util.{Failure, Success, Try}
import java.nio.file.NoSuchFileException

object FileService {
private[this] val logger = getLogger

type PathCollector[F[_]] = (File, Config[F], Request[F]) => OptionT[F, Response[F]]

/** [[org.http4s.server.staticcontent.FileService]] configuration
Expand Down Expand Up @@ -43,14 +51,46 @@ object FileService {
}

/** Make a new [[org.http4s.HttpRoutes]] that serves static files. */
private[staticcontent] def apply[F[_]](config: Config[F])(implicit F: Effect[F]): HttpRoutes[F] =
Kleisli {
case request if request.pathInfo.startsWith(config.pathPrefix) =>
getFile(s"${config.systemPath}/${getSubPath(request.pathInfo, config.pathPrefix)}")
.flatMap(f => config.pathCollector(f, config, request))
.semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
case _ => OptionT.none
private[staticcontent] def apply[F[_]](config: Config[F])(
implicit F: Effect[F]): HttpRoutes[F] = {
object BadTraversal extends Exception with NoStackTrace
Try(Paths.get(config.systemPath).toRealPath()) match {
case Success(rootPath) =>
TranslateUri(config.pathPrefix)(Kleisli {
case request =>
request.pathInfo.split("/") match {
case Array(head, segments @ _*) if head.isEmpty =>
OptionT
.liftF(F.catchNonFatal {
segments.foldLeft(rootPath) {
case (_, "" | "." | "..") => throw BadTraversal
case (path, segment) =>
path.resolve(Uri.decode(segment, plusIsSpace = true))
}
})
.semiflatMap(path => F.delay(path.toRealPath(LinkOption.NOFOLLOW_LINKS)))
.collect { case path if path.startsWith(rootPath) => path.toFile }
.flatMap(f => config.pathCollector(f, config, request))
.semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
.recoverWith {
case _: NoSuchFileException => OptionT.none
case BadTraversal => OptionT.some(Response(Status.BadRequest))
}
case _ => OptionT.none
}
})

case Failure(_: NoSuchFileException) =>
logger.error(
s"Could not find root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will return none.")
Kleisli(_ => OptionT.none)

case Failure(e) =>
logger.error(e)(
s"Could not resolve root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will fail with a 500.")
Kleisli(_ => OptionT.pure(Response(Status.InternalServerError)))
}
}

private def filesOnly[F[_]](file: File, config: Config[F], req: Request[F])(
implicit F: Sync[F],
Expand Down Expand Up @@ -124,12 +164,4 @@ object FileService {

case _ => OptionT.none
}

// Attempts to sanitize the file location and retrieve the file. Returns None if the file doesn't exist.
private def getFile[F[_]](unsafePath: String)(implicit F: Sync[F]): OptionT[F, File] =
OptionT(F.delay {
val f = new File(Uri.removeDotSegments(unsafePath))
if (f.exists()) Some(f)
else None
})
}
Expand Up @@ -4,9 +4,16 @@ package staticcontent

import cats.data.{Kleisli, OptionT}
import cats.effect._
import cats.implicits._
import java.nio.file.Paths
import org.http4s.server.middleware.TranslateUri
import org.log4s.getLogger
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success, Try}
import scala.util.control.NoStackTrace

object ResourceService {
private[this] val logger = getLogger

/** [[org.http4s.server.staticcontent.ResourceService]] configuration
*
Expand All @@ -26,18 +33,49 @@ object ResourceService {
preferGzipped: Boolean = false)

/** Make a new [[org.http4s.HttpRoutes]] that serves static files. */
private[staticcontent] def apply[F[_]: Effect: ContextShift](config: Config[F]): HttpRoutes[F] =
Kleisli {
case request if request.pathInfo.startsWith(config.pathPrefix) =>
StaticFile
.fromResource(
Uri.removeDotSegments(
s"${config.basePath}/${getSubPath(request.pathInfo, config.pathPrefix)}"),
config.blockingExecutionContext,
Some(request),
preferGzipped = config.preferGzipped
)
.semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
case _ => OptionT.none
private[staticcontent] def apply[F[_]](
config: Config[F])(implicit F: Effect[F], cs: ContextShift[F]): HttpRoutes[F] = {
val basePath = if (config.basePath.isEmpty) "/" else config.basePath
object BadTraversal extends Exception with NoStackTrace

Try(Paths.get(basePath)) match {
case Success(rootPath) =>
TranslateUri(config.pathPrefix)(Kleisli {
case request =>
request.pathInfo.split("/") match {
case Array(head, segments @ _*) if head.isEmpty =>
OptionT
.liftF(F.catchNonFatal {
segments.foldLeft(rootPath) {
case (_, "" | "." | "..") => throw BadTraversal
case (path, segment) =>
path.resolve(Uri.decode(segment, plusIsSpace = true))
}
})
.collect {
case path if path.startsWith(rootPath) => path
}
.flatMap { path =>
StaticFile.fromResource(
path.toString,
config.blockingExecutionContext,
Some(request),
preferGzipped = config.preferGzipped
)
}
.semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
.recoverWith {
case BadTraversal => OptionT.some(Response(Status.BadRequest))
}
case _ =>
OptionT.none
}
})

case Failure(e) =>
logger.error(e)(
s"Could not get root path from ResourceService config: basePath = ${config.basePath}, pathPrefix = ${config.pathPrefix}. All requests will fail.")
Kleisli(_ => OptionT.pure(Response(Status.InternalServerError)))
}
}
}
Expand Up @@ -4,7 +4,10 @@ package staticcontent

import cats.data.{Kleisli, OptionT}
import cats.effect.{ContextShift, Effect}
import cats.implicits._
import java.nio.file.{Path, Paths}
import scala.concurrent.ExecutionContext
import scala.util.control.NoStackTrace

/**
* Constructs new services to serve assets from Webjars
Expand Down Expand Up @@ -54,16 +57,32 @@ object WebjarService {
* @param config The configuration for this service
* @return The HttpRoutes
*/
def apply[F[_]: Effect: ContextShift](config: Config[F]): HttpRoutes[F] = Kleisli {
// Intercepts the routes that match webjar asset names
case request if request.method == Method.GET =>
OptionT
.pure[F](request.pathInfo)
.map(Uri.removeDotSegments)
.subflatMap(toWebjarAsset)
.filter(config.filter)
.flatMap(serveWebjarAsset(config, request)(_))
case _ => OptionT.none
def apply[F[_]](config: Config[F])(implicit F: Effect[F], cs: ContextShift[F]): HttpRoutes[F] = {
object BadTraversal extends Exception with NoStackTrace
val Root = Paths.get("")
Kleisli {
// Intercepts the routes that match webjar asset names
case request if request.method == Method.GET =>
request.pathInfo.split("/") match {
case Array(head, segments @ _*) if head.isEmpty =>
OptionT
.liftF(F.catchNonFatal {
segments.foldLeft(Root) {
case (_, "" | "." | "..") => throw BadTraversal
case (path, segment) =>
path.resolve(Uri.decode(segment, plusIsSpace = true))
}
})
.subflatMap(toWebjarAsset)
.filter(config.filter)
.flatMap(serveWebjarAsset(config, request)(_))
.recover {
case BadTraversal => Response(Status.BadRequest)
}
case _ => OptionT.none
}
case _ => OptionT.none
}
}

/**
Expand All @@ -72,14 +91,17 @@ object WebjarService {
* @param subPath The request path without the prefix
* @return The WebjarAsset, or None if it couldn't be mapped
*/
private def toWebjarAsset(subPath: String): Option[WebjarAsset] =
Option(subPath)
.map(_.split("/", 4))
.collect {
case Array("", library, version, asset)
if library.nonEmpty && version.nonEmpty && asset.nonEmpty =>
WebjarAsset(library, version, asset)
}
private def toWebjarAsset(p: Path): Option[WebjarAsset] = {
val count = p.getNameCount
if (count > 2) {
val library = p.getName(0).toString
val version = p.getName(1).toString
val asset = p.subpath(2, count)
Some(WebjarAsset(library, version, asset.toString))
} else {
None
}
}

/**
* Returns an asset that matched the request if it's found in the webjar path
Expand Down
1 change: 1 addition & 0 deletions server/src/test/resources/Dir/partial-prefix.txt
@@ -0,0 +1 @@
I am useful to test leaks from prefix paths.
@@ -0,0 +1,5 @@
Come on
Come on
Come on
Let's go
Space truckin'
5 changes: 5 additions & 0 deletions server/src/test/resources/space truckin'.txt
@@ -0,0 +1,5 @@
Come on
Come on
Come on
Let's go
Space truckin'
1 change: 1 addition & 0 deletions server/src/test/resources/symlink
1 change: 1 addition & 0 deletions server/src/test/resources/test/keep.txt
@@ -0,0 +1 @@
.

0 comments on commit 752b3f6

Please sign in to comment.