GitHub Security Advisory: GHSA-wcwh-7gfw-5wrr
Package: org.http4s:http4s-ember-core_2.13 (MAVEN)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
Vulnerable versions: < 0.23.31
Patched in: 0.23.31

Summary: Http4s vulnerable to HTTP Request Smuggling due to improper handling of HTTP trailer section

Details:
### Summary
http4s is vulnerable to HTTP Request Smuggling due to improper handling of HTTP trailer section.
This vulnerability could enable attackers to:
- Bypass front-end servers security controls
- Launch targeted attacks against active users
- Poison web caches

Pre-requisites for the exploitation: the web appication has to be deployed behind a reverse-proxy that forwards trailer headers.

### Details
The HTTP chunked message parser, after parsing the last body chunk, calls `parseTrailers` (`ember-core/shared/src/main/scala/org/http4s/ember/core/ChunkedEncoding.scala#L122-142`).
This method parses the trailer section using `Parser.parse`, where the issue originates.

`parse` has a bug that allows to terminate the parsing before finding the double CRLF condition: when it finds an header line that **does not include the colon character**, it continues parsing with `state=false` looking for the header name till reaching the condition `else if (current == lf && (idx > 0 && message(idx - 1) == cr))` that sets `complete=true` even if no `\r\n\r\n` is  found.
```scala
if (current == colon) {
  state = true // set state to check for header value
  name = new String(message, start, idx - start) // extract name string
  start = idx + 1 // advance past colon for next start

  // TODO: This if clause may not be necessary since the header value parser trims
  if (message.size > idx + 1 && message(idx + 1) == space) {
    start += 1 // if colon is followed by space advance again
    idx += 1 // double advance index here to skip the space
  }
  // double CRLF condition - Termination of headers
} else if (current == lf && (idx > 0 && message(idx - 1) == cr)) { // <----- not a double CRLF check
  complete = true // completed terminate loop
}
```
The remainder left in the buffer is then parsed as another request leading to HTTP Request Smuggling.

### PoC

Start a simple webserver that echoes the received requests:
```scala
import cats.effect._
import cats.implicits._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Router
import org.http4s.server.middleware.RequestLogger
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory
import com.comcast.ip4s._

object ExploitServer extends IOApp {

  implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]

  val echoService: HttpRoutes[IO] = HttpRoutes.of[IO] {
    case req @ _ =>
      for {
        bodyStr <- req.bodyText.compile.string
        method = req.method.name
        uri = req.uri.toString()
        version = req.httpVersion.toString
        headers = req.headers.headers.map { header =>
          s"${header.name.toString.toLowerCase}: ${header.value}"
        }.mkString("\n")
        
        responseText = s"""$method $uri $version
$headers

$bodyStr

"""
        result <- Ok(responseText)
      } yield result
  }

  val httpApp = RequestLogger.httpApp(logHeaders = true, logBody =