Skip to content
Permalink
Browse files Browse the repository at this point in the history
Merge pull request from GHSA-66q9-f7ff-mmx6
  • Loading branch information
rossabaker committed Mar 25, 2020
2 parents 8be70c6 + 1c48c57 commit b87f31b
Show file tree
Hide file tree
Showing 16 changed files with 439 additions and 70 deletions.
8 changes: 8 additions & 0 deletions build.sbt
Expand Up @@ -63,6 +63,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 serverMetrics = libraryProject("server-metrics")
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
@@ -1 +1 @@
sbt.version=1.2.3
sbt.version=1.3.8
2 changes: 1 addition & 1 deletion project/plugins.sbt
Expand Up @@ -4,7 +4,7 @@ addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.2")
addSbtPlugin("com.github.tkawachi" % "sbt-doctest" % "0.7.2")
addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.15")
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.18")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.7.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.15")
addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.2")
Expand Down
Expand Up @@ -4,12 +4,22 @@ package staticcontent

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.http4s.util.UrlCodingUtils.urlDecode
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 @@ -42,14 +52,46 @@ object FileService {
}

/** Make a new [[org.http4s.HttpService]] that serves static files. */
private[staticcontent] def apply[F[_]](config: Config[F])(implicit F: Effect[F]): HttpService[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]): HttpService[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(urlDecode(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]): OptionT[F, Response[F]] =
Expand Down Expand Up @@ -95,12 +137,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(PathNormalizer.removeDotSegments(unsafePath))
if (f.exists()) Some(f)
else None
})
}
Expand Up @@ -4,9 +4,17 @@ 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.http4s.util.UrlCodingUtils.urlDecode
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,17 +34,48 @@ object ResourceService {
preferGzipped: Boolean = false)

/** Make a new [[org.http4s.HttpService]] that serves static files. */
private[staticcontent] def apply[F[_]: Effect](config: Config[F]): HttpService[F] =
Kleisli {
case request if request.pathInfo.startsWith(config.pathPrefix) =>
StaticFile
.fromResource(
PathNormalizer.removeDotSegments(
s"${config.basePath}/${getSubPath(request.pathInfo, config.pathPrefix)}"),
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]): HttpService[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(urlDecode(segment, plusIsSpace = true))
}
})
.collect {
case path if path.startsWith(rootPath) => path
}
.flatMap { path =>
StaticFile.fromResource(
path.toString,
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,6 +4,10 @@ package staticcontent

import cats.data.{Kleisli, OptionT}
import cats.effect.Effect
import cats.implicits._
import java.nio.file.{Path, Paths}
import org.http4s.util.UrlCodingUtils.urlDecode
import scala.util.control.NoStackTrace

/**
* Constructs new services to serve assets from Webjars
Expand Down Expand Up @@ -51,16 +55,32 @@ object WebjarService {
* @param config The configuration for this service
* @return The HttpService
*/
def apply[F[_]: Effect](config: Config[F]): HttpService[F] = Kleisli {
// Intercepts the routes that match webjar asset names
case request if request.method == Method.GET =>
OptionT
.pure[F](request.pathInfo)
.map(PathNormalizer.removeDotSegments)
.subflatMap(toWebjarAsset)
.filter(config.filter)
.flatMap(serveWebjarAsset(config, request)(_))
case _ => OptionT.none
def apply[F[_]](config: Config[F])(implicit F: Effect[F]): HttpService[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(urlDecode(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 @@ -69,14 +89,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 b87f31b

Please sign in to comment.