VYPR
Moderate severityNVD Advisory· Published Sep 23, 2025· Updated Sep 23, 2025

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

CVE-2025-59822

Description

Http4s is a Scala interface for HTTP services. In versions from 1.0.0-M1 to before 1.0.0-M45 and before 0.23.31, 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, and poison web caches. A pre-requisite for exploitation involves the web application being deployed behind a reverse-proxy that forwards trailer headers. This issue has been patched in versions 1.0.0-M45 and 0.23.31.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Http4s HTTP request smuggling vulnerability via improper trailer parsing allows attackers to bypass security controls, poison caches, and target users when deployed behind a reverse-proxy.

Vulnerability

Description

Http4s, a Scala HTTP interface, is affected by an HTTP request smuggling vulnerability arising from improper handling of the HTTP trailer section during chunked transfer encoding [1]. The issue resides in the parseTrailers method of the chunked encoding parser, which calls Parser.parse to process trailer headers. A bug in parse allows parsing to terminate prematurely upon encountering a header line without a colon, bypassing the double CRLF termination condition [3]. This affects versions from 1.0.0-M1 to before 1.0.0-M45 and all versions before 0.23.31 [1].

Exploitation

Prerequisites

Exploitation requires the application to be deployed behind a reverse-proxy that forwards trailer headers [1]. An attacker can send a specially crafted HTTP request with a malformed trailer section that exploits the premature termination, causing the parser to treat part of the request body as the beginning of the next request [3].

Impact

Successful exploitation can allow an attacker to bypass front-end security controls, launch targeted attacks against active users, and poison web caches [3]. This could lead to session hijacking, cache poisoning, and other HTTP desynchronization attacks.

Mitigation

The vulnerability has been patched in http4s versions 1.0.0-M45 and 0.23.31 [1]. The fix involves stricter validation of header fields, rejecting invalid whitespace characters around field names as seen in the referenced commit [2]. Users are advised to upgrade to the patched versions immediately.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.http4s:http4s-ember-core_2.12Maven
< 0.23.310.23.31
org.http4s:http4s-ember-core_2.13Maven
< 0.23.310.23.31
org.http4s:http4s-ember-core_3Maven
< 0.23.310.23.31
org.http4s:http4s-ember-core_2.13Maven
>= 1.0.0-M1, < 1.0.0-M451.0.0-M45
org.http4s:http4s-ember-core_3Maven
>= 1.0.0-M1, < 1.0.0-M451.0.0-M45

Affected products

2
  • Http4k/Http4kllm-fuzzy
    Range: <0.23.31, >=1.0.0-M1, <1.0.0-M45
  • http4s/http4sv5
    Range: < 0.23.31

Patches

1
dd518f7c967e

fail parsing invalid whitespace around field name

https://github.com/http4s/http4sSam PillsworthSep 12, 2025via ghsa
3 files changed · +95 15
  • ember-core/shared/src/main/scala/org/http4s/ember/core/Parser.scala+15 15 modified
    @@ -27,6 +27,7 @@ import scodec.bits.ByteVector
     
     import scala.annotation.switch
     import scala.util.control.NonFatal
    +import scala.util.control.NoStackTrace
     
     private[ember] object Parser {
     
    @@ -116,21 +117,17 @@ private[ember] object Parser {
     
           while (!complete && idx <= upperBound) {
             if (!state) {
    -          val current = message(idx)
    -          // if current index is colon our name is complete
    -          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)) {
    -            complete = true // completed terminate loop
    +          (message(idx): @switch) match {
    +            case ':' =>
    +              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
    +            case '\r' if start == idx => // proceed
    +            case '\n' if idx > 0 && message(idx - 1) == cr => complete = true
    +            case c if c <= 0x20 | c == 0x7f =>
    +              throwable = InvalidHeaderWhitespace
    +              complete = true
    +            case _ => // proceed
               }
             } else {
               val current = message(idx)
    @@ -182,6 +179,9 @@ private[ember] object Parser {
           }
         }
     
    +    case object InvalidHeaderWhitespace extends Exception with NoStackTrace {
    +      override val getMessage = "InvalidHeaderWhitespace"
    +    }
         final case class ParseHeadersError(cause: Throwable)
             extends Exception(
               s"Encountered Error Attempting to Parse Headers - ${cause.getMessage}",
    
  • ember-core/shared/src/test/scala/org/http4s/ember/core/ParserSuite.scala+73 0 modified
    @@ -23,6 +23,7 @@ import cats.syntax.all._
     import fs2._
     import org.http4s._
     import org.http4s.headers.Expires
    +import org.http4s.ember.core.Parser.HeaderP.ParseHeadersError
     import org.http4s.implicits._
     import org.typelevel.ci._
     import scodec.bits.ByteVector
    @@ -254,6 +255,78 @@ class ParsingSuite extends Http4sSuite {
         } yield req1._1.method == Method.GET && req2._1.method == Method.GET).assert
       }
     
    +  test("Parser.Request.parser should fail on invalid whitespace") {
    +    val defaultMaxHeaderLength = 4096
    +    val reqS = Stream(
    +      "POST /foo HTTP/1.1\r\na\r\n"
    +    )
    +    val byteStream: Stream[IO, Byte] = reqS.flatMap(s =>
    +      Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII)))
    +    )
    +
    +    for {
    +      take <- Helpers.taking[IO, Byte](byteStream)
    +      _ <- interceptMessageIO[ParseHeadersError](
    +        "Encountered Error Attempting to Parse Headers - InvalidHeaderWhitespace"
    +      ) {
    +        Parser.Request.parser[IO](defaultMaxHeaderLength)(Array.emptyByteArray, take)
    +      }
    +    } yield ()
    +  }
    +
    +  test("Parser.Request.parser should handle correct whitespace split across chunks") {
    +    val defaultMaxHeaderLength = 4096
    +    val raw1 = "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n"
    +    val raw2 = "\r\n2\r\naa\r\n\r\n0\r\nTrailer: header\r"
    +    val raw3 = "\n\r\n"
    +
    +    val byteStream: Stream[IO, Byte] =
    +      (Stream(raw1) ++ Stream(raw2) ++ Stream(raw3)).through(fs2.text.utf8.encode)
    +
    +    for {
    +      take <- Helpers.taking[IO, Byte](byteStream)
    +      req <- Parser.Request.parser[IO](defaultMaxHeaderLength)(Array.emptyByteArray, take)
    +      body <- req._1.body.through(text.utf8.decode).compile.string
    +      th <- req._1.trailerHeaders
    +    } yield {
    +      assertEquals(req._1.method, Method.POST)
    +      assertEquals(body, "aa")
    +      assertEquals(th.headers, List(Header.Raw(ci"Trailer", "header")))
    +    }
    +  }
    +
    +  test(
    +    "Parser.Request.parser should fail to parse a chunked body with malformed trailer headers"
    +  ) {
    +    val defaultMaxHeaderLength = 4096
    +    val reqS =
    +      Stream(
    +        "POST /foo HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n",
    +        "\r\n",
    +        "2\r\n",
    +        "aa\r\n",
    +        "\r\n",
    +        "0\r\nTrailer: header\r\n",
    +        "a\r\n",
    +      )
    +
    +    val byteStream: Stream[IO, Byte] = reqS
    +      .flatMap(s =>
    +        Stream.chunk(Chunk.array(s.getBytes(java.nio.charset.StandardCharsets.US_ASCII)))
    +      )
    +
    +    for {
    +      take <- Helpers.taking[IO, Byte](byteStream)
    +      req1 <- Parser.Request.parser[IO](defaultMaxHeaderLength)(Array.emptyByteArray, take)
    +      _ <- interceptMessageIO[ParseHeadersError](
    +        "Encountered Error Attempting to Parse Headers - InvalidHeaderWhitespace"
    +      ) {
    +        // drain the body to trigger the trailing header parsing, which will raise an exception
    +        req1._1.body.through(text.utf8.decode).compile.string
    +      }
    +    } yield ()
    +  }
    +
       test("Parser.Response.parser should handle a chunked response") {
         val defaultMaxHeaderLength = 4096
         val base =
    
  • ember-server/shared/src/main/scala/org/http4s/ember/server/internal/ServerHelpers.scala+7 0 modified
    @@ -55,6 +55,9 @@ private[server] object ServerHelpers extends ServerHelpersPlatform {
       private val serverFailure =
         Response(Status.InternalServerError).putHeaders(org.http4s.headers.`Content-Length`.zero)
     
    +  private val badRequest =
    +    Response(Status.BadRequest).putHeaders(org.http4s.headers.`Content-Length`.zero)
    +
       def server[F[_]](
           host: Option[Host],
           port: Port,
    @@ -389,6 +392,9 @@ private[server] object ServerHelpers extends ServerHelpersPlatform {
           requestVault <- if (createRequestVault) mkRequestVault(socket) else Vault.empty.pure[F]
           resp <- httpApp
             .run(req.withAttributes(requestVault))
    +        .recover { case Parser.HeaderP.ParseHeadersError(_) =>
    +          badRequest.covary[F]
    +        }
             .handleErrorWith(errorHandler)
             .handleError(_ => serverFailure.covary[F])
         } yield (req, resp, drain)
    @@ -534,6 +540,7 @@ private[server] object ServerHelpers extends ServerHelpersPlatform {
                     Applicative[F].pure(None)
                   case err =>
                     (err match {
    +                  case err: Parser.HeaderP.ParseHeadersError => requestLineParseErrorHandler(err)
                       case err: Parser.Request.ReqPrelude.ParsePreludeError =>
                         requestLineParseErrorHandler(err)
                       case err: EmberException.MessageTooLong =>
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.